Skip to content

Ecce Signum

Immanentize the Empathy

  • Home
  • About Me
  • Published Works and Literary Matters
  • Indexes
  • Laboratory
  • Notebooks
  • RSS Feed

Category: Programming

HTML5 Canvas Drawing Library Exploration: Processing.js

2012-02-13 John Winkelman

This is the fifth in a loose series of articles exploring some of the Javascript drawing libraries currently freely available to developers.

Today I will be experimenting with Processing.js, which is a radical departure from the previous libraries in this series.

Processing.js started out as Processing, a Java graphics library/custom language which can trace its origins back to the year 2001 and the big brains of Casey Reas and Ben Fry at the MIT Media Lab. When I first discovered Flash, my quest for source code and examples repeatedly brought me in contact with Proce55ing (as it was called back then). Many of my early coding experiments revolved around trying to re-create in Flash that which had been so wonderfully demonstrated in Java.

And now almost everything which has been done in Java can now be done in Javascript, with little change to the original source code. That’s right – Processing.js can use the same Processing code that the Java version uses.

Click here to launch the demo. Give it a moment; the Processing.js library is over 200kb.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Trigonometron in Processing.js</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;}
			#myCanvas {width:800px;height:480px;position:absolute;left:0;top:0;}
		</style>
		<script type="text/javascript" src="processing-1.3.6.min_.js"></script>
	</head>
	<body>
		<canvas id="myCanvas" data-processing-sources="/path/to/trigonometron.pde"></canvas>
	</body>
</html>

Nothing too exciting here. The Processing.js library is loaded in the <head/> element. The <canvas/> tag has a custom attribute, “data-processing-sources”. The value for this attribute should be the path – relative or absolute – to the Processing source file (*.pde) used in this app.

Processing code

String currentShape="circle";
boolean isPaused = false;
boolean isInitialized = false;


String[] shapes = {"circle","tricuspoid","tetracuspoid","epicycloid","epicycloid 2","epicycloid 3","lissajous","lemniscate","butterfly"};
int w = 800;
int h = 480;
int centerX = 240;
int centerY = 240;
int radius_x = 150;
int radius_y = 150;
int theta = 0;
ArrayList objects;
ArrayList buttons;
int numObjects = 0;
float r2d = 180/PI;
float d2r = PI/180;
int orbitSteps = 180;
float orbitSpeed = PI*2/orbitSteps;
float objectInterval;
float objectPosition;
int direction = 1;
int index = 0;
int xVar1 = 0;
int xVar2;
int xVar3;
int xVar4;
int startingObjects = 100;
float newX;
float newY;

PFont buttonFont;

void setup(){
	size(800,480);
	background(255,255,255);
	fill(30);
	PFont buttonFont = loadFont("courier");
	textFont(buttonFont,14);
	textAlign(CENTER);
	objects = new ArrayList();
	buttons = new ArrayList();
	initInterface();
	initObjects();
	frameRate(32);
}

void initObjects() {
	for(int i=0;i<startingObjects;i++) {
		addObject();
	}
}
void addObject() {
	objects.add(new CircleObject(centerX,centerY,10,10));
	numObjects = objects.size();
	objectInterval = orbitSteps/numObjects;
}

void initInterface() {
	int xOff = 625;
	int yOff = 25;
	for(int i=0;i<shapes.length;i++) {
		buttons.add(new TextButton(xOff,yOff,150,20,shapes[i]));
		yOff += 23;
	}
	yOff += 23;
	buttons.add(new TextButton(xOff,yOff,150,20,"PLAY/PAUSE"));
}
void draw() {
	if(isPaused==true) return;
	background(255,255,255);
	for(int i=0;i<numObjects;i++) {
		objectPosition = orbitSpeed*objectInterval*i;
		switch(currentShape) {
			case "circle":
				newX = centerX + radius_x * cos(theta + objectPosition);
				newY = centerY + radius_y * sin(theta + objectPosition);
				break;
			case "tricuspoid":
				newX = centerX + (radius_x*.5) * ((2 * cos(theta + objectPosition)) + cos(2 * (theta + objectPosition)));
				newY = centerY + (radius_y*.5) * ((2 * sin(theta + objectPosition)) - sin(2 * (theta + objectPosition)));
				break;
			case "tetracuspoid":
				newX = centerX + radius_x * pow((cos(theta + objectPosition)),3);
				newY = centerY + radius_y * pow((sin(theta + objectPosition)),3);
				break;
			case "epicycloid":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 1) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 1) * (theta + objectPosition)));
				break;
			case "epicycloid 2":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 2) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 2) * (theta + objectPosition)));
				break;
			case "epicycloid 3":
				newX = centerX + (radius_x*.4) * cos(theta + objectPosition) - radius_x*1*(cos((radius_x/radius_x + 3) * (theta + objectPosition)));
				newY = centerY + (radius_y*.4) * sin(theta + objectPosition) - radius_y*1*(sin((radius_y/radius_y + 3) * (theta + objectPosition)));
				break;
			case "lissajous":
				newX = centerX + radius_x * (sin(3 * (theta + objectPosition) + xVar1));
				newY = centerY + radius_y * sin(theta + objectPosition);
				xVar1 += .002;
				break;
			case "lemniscate":
				newX = centerX + (radius_x*1.2) * ((cos(theta + objectPosition)/(1 + pow(sin(theta + objectPosition),2))));
				newY = centerY + (radius_y*1.2) * (sin(theta + objectPosition) * (cos(theta + objectPosition)/(1 + pow(sin(theta + objectPosition),2))));
				break;
			case "butterfly":
				newX = centerX + (radius_x*.4) * (cos(theta + objectPosition) * (pow(5,cos(theta+objectPosition)) - 2 * cos(4 * (theta+objectPosition)) - pow(sin((theta+objectPosition)/12),4)));
				newY = centerY + (radius_y*.4) * (sin(theta + objectPosition) * (pow(5,cos(theta+objectPosition)) - 2 * cos(4 * (theta+objectPosition)) - pow(sin((theta+objectPosition)/12),4)));
				break;
			default:
				break;
	
		}
		objects.get(i).updatePosition(newX,newY);
	}
	theta += (orbitSpeed*direction);
	
	for(int j = 0;j<buttons.size();j++) {
		buttons.get(j).drawMe();
	}
}

void mouseClicked() {
	for(int i=0;i<buttons.size();i++) {
		buttons.get(i).onClick(mouseX,mouseY);
	}
}
void mouseMoved() {
	for(int i=0;i<buttons.size();i++) {
		buttons.get(i).onHover(mouseX,mouseY);
	}
}

class CircleObject {
	float x,y,d;
	int colorR,colorG,colorB;
	CircleObject(cX,cY,cW,cH) {
		x = cX;
		y = cY;
		w = cW;
		h = cH;
		colorR = random(255);
		colorG = random(255);
		colorB = random(255);
		drawMe();
	}
	void updatePosition(nX,nY) {
		x = nX;
		y = nY;
		drawMe();
	}
	void drawMe() {
		fill(colorR,colorG,colorB);
		stroke(128,128,128);
		ellipse(x,y,w,h);
	}
}

class TextButton {
	int x, y, w, h;
	string btnTxt;
	int overGray = 255;
	int offGray = 238;
	int currentGray = offGray;
	TextButton(int cX,int cY,int cW,int cH,string tx) {
		x = cX;
		y = cY;
		w = cW;
		h = cH;
		btnTxt = tx;
		drawMe();
	}
	void onClick(int mX,int mY) {
		if(mX>x && mY>y && mX < (x+w) && mY < (y+h)) {
			if(btnTxt=="PLAY/PAUSE"){
				isPaused = !isPaused;
			} else {
				currentShape = btnTxt;
			} 
		}
	}
	void onHover(int mX,int mY) {
		if(mX>x && mY>y && mX < (x+w) && mY < (y+h)) {
			cursor(HAND);
			currentGray = overGray;
		} else {
			cursor(ARROW);
			currentGray = offGray;
		}
	}
	void drawMe() {
		stroke(128,128,128);
		fill(currentGray,currentGray,currentGray);
		rect(x,y,w,h);
		fill(0,0,0);
		text(btnTxt,x,y+5,w,h);
	}
}

And here is where things become radically different from the previous examples. This is Processing script, which is essentially a custom, simplified version of Java. There is a lot going on here; more than with any of the previous examples. I will be going into extra detail in the following section.

  • 1-3 – instantiate global variables
  • 6-31 – instantiate variables specific to the animation. Note the different syntax used here – int, float, ArrayList, String, rather than simply declaring “var x = y”.
  • 33 – declare a variable to hold the font for the buttons
  • 35-47 – setup() – built-in Processing method. This is where the environment is set up, variables are initialized, and any external assets are referenced
    • 36 – set the width and height of the app
    • 37-38 – set the color of the background and fill it in with an alpha value of 30% opaque
    • 39 – set the base font for the app
    • 40 – set the size of the base font
    • 41 – set the base text alignment
    • 42-43 instantiate the array lists which will hold the buttons for the UI and the objects for the animations
    • 44-45 call the methods which will create the UI buttons and the objects for the animations
    • 46 – set the frame rate of the app to 32 frames per second.
  • 49-53 – initObjects() – create a number of objects for the animations, equal to the value of the variable startingObjects
  • 54-58 – addObject() – create a circle graphic for the animations
    • 55 – create an instance of the CircleObject class, and add it to the objects ArrayList
    • 56 – update the numObjects variable to equal the length of the objects ArrayList
    • 57 – adjust the spacing of the circle graphics in the animations, based on the number of circles
  • 60-69 – initInterface() – create and position the user interface buttons
    • 61-62 set the base X and Y positions for the buttons in the UI
    • 63-66 – create one TextButton instance for each element in the shapes array
    • 67 – increment the Y position for the play/pause button
    • 68 – create a TextButton instance for the play/pause button
  • 70-124 draw() – built-in Processing method. This is where the various objects are drawn on the screen. If the frameRate is set, then this method is called frameRate() times per second (see line 46).
    • 71 – if the animation is paused, do nothing
    • 72 – fill in the canvas with white
    • 73-119 cycle through and update the positions of each object in the animation
      • 74 – get the base position of one of the instances of the circle graphics
      • 75-116 – based on the current value of currentShape, find the new x/y position for the object
      • 117 – call the updatePosition() method of the object, and feed it the new x/y coordinates
    • 119 – update the rotation of the animation
    • 121-123 – iterate through the UI buttons and re-draw them to the screen
  • 126-130 – mouseClicked() – built-in Processing method. Called whenever a mouse click is detected in the app
    • 127-129 – cycle through all of the UI buttons and call the onClick() method of each, using the mouse X and Y coordinates as arguments.
  • 131-135 – mouseMoved() – built-in Processing method. Called whenever the mouse changes position in the app
    • 132-134 – cycle through all of the UI buttons and call the onHover() method of each, using the mouse X and Y coordinates as arguments.
  • 137-160 – class CircleObject{} – contains properties and methods of the circle graphics used to display the animations
    • 138-139 – initialize internal variables for this class
    • 140-149 – CircleObject() – constructor for creating instances of the CircleObject class. Also see line 55.
      • 141-144 – set internal variables to the arguments fed into the constructor
      • 145-147 – set random values for the red, green, and blue components of the fill color for this object
      • 148 – call the drawMe() method of this class
    • 150-154 – updatePosition() – changes the x and y position of this object
      • 151-152 – update the x and y variables in this object
      • 153 – call the drawMe() method to draw this object to the screen
    • 155-159 – drawMe() – method which draws the circle graphic to the screen. Also see lines 148 and 153
      • 156 – set the fill color of the circle graphic using the red, green, and blue values which were defined in the constructor
      • 157 – set the stroke color to medium gray
      • 158 – draw the circle (ellipse) to the screen
  • 162-201 – class TextButton{} – contains properties and methods for displaying UI buttons and handling mouse interactions
    • 163-167 – initialize internal variables for this class
    • 168-175 – TextButton() – constructor method for creating instances of the TextButton class. Also see lines 64 and 68
      • 169-173 set the values of the internal variables
      • 174 – call the drawMe() method of this class to draw an instance of this button to the screen
    • 176-184 – onClick() – check to see if the mouse was over this instance when it was clicked
      • 177-183 – check to see if the mouse x and y coordinates fall within the x/y and width/height area of this button
        • 178-179 – if this is the play/pause button, toggle the “isPaused” global variable
        • 180-183 – otherwise, set the global “currentShape” property to equal the value of this button
    • 185-193 – onHover() – check to see of the mouse was over this button instance when it moved
      • 186-188 – if the mouse x and y position was within the bounds of this button, set its background color to the hover color, and change the cursor to a hand
      • 189-192 – otherwise, set the background color to the default gray and change the cursor to an arrow
    • 194-200 – drawMe() – draw this instance of the button to the screen
      • 195 – set the stroke color to a medium gray
      • 196 – set the fill color for the background of this button
      • 197 – draw a gray rectangle
      • 198 – set the text color to black
      • 199 – draw the text for this button to the screen

And there it is. Probably the most complex code in this series. This is why libraries like Easel and oCanvas are so nice for simpler applications – they take all of the hassle out of creating UI elements.

Processing.js is a very big gun as far as drawing libraries go. It is perhaps not the best one to use for basic UI work, but if you have something which requires serious visualization power, such as representations of complex mathematics or scientific data, then this is the tool to use. It can easily interface with vanilla Javascript, so if you don’t want to go through the trouble of coding your own buttons, you can create the UI in plain HTML and use, say, jQuery to hook everything together.

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

Previous articles: Easel.js, oCanvas.js, Raphael.js, Paper.js.

Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: Processing.js

HTML5 Canvas Drawing Library Exploration: Paper.js

2012-02-09 John Winkelman

This is the fourth in an ongoing series of posts exploring some Javascript drawing libraries. Earlier posts cover Easel, oCanvas, and Raphael.

Paper.js is an interesting library. It is a Javascript port of the Scriptographer tool used in Adobe Illustrator, and much effort has been made to keep the code used in each tool interchangeable. It does a good job of opening the programming door for illustrators looking to create designs which, if created by hand, would be labor-intensive and/or complex to the point of being nearly impossible.

The code for Paper.js is a little more complex than in the previous libraries I have covered. Mouse interactions are a little more take a little more work to set up, and the number of drawing tools is quite large. Paper.js allows the creation of a custom DOM within the canvas element, and that makes it possible to create quite UI-intensive applications.

Click to Launch the demo.

HTML

<!DOCTYPE html>
<html>
	<head>
		<title>Paper.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="paper.js"></script>
	</head>
	<body>
		<canvas id="myCanvas" width="800" height="480"></canvas>
		<script type="text/paperscript" canvas="myCanvas">
			//	paperscript code goes here
		</script>
	</body>
</html>

The HTML is mostly identical to that used in the Easel and oCanvas demos, with one exception: Notice the <script/> element in the body. It has a custom type (“text/paperscript”), and a custom attribute (“canvas”), which are used by the Paper.js library to set up the page. Paperscript is simply Javascript with some additional properties and methods. It is possible to use Paper.js with pure Javascript, and the only difference would be some more verbose coding.

Javascript/Paperscript

/*	global variables	*/
var currentShape="circle";
var	isPaused = true;
var	isInitialized = false;
var buttons = [];
var pauseButton;

/*	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 = 0;
var xVar3 = 0;
var xVar4 = 0;
var startingObjects = 100;
var newX;
var newY;

initInterface();
initObjects();
onFrame();
isInitialized = true;
function initInterface() {
	var xOff = 650;
	var yOff = 25;
	for(var i=0;i<shapes.length;i++) {
		var bGroup = new Group();
		var btn = new Rectangle(new Point(0,0),new Size(150,20));
		var btnPath = new Path.Rectangle(btn);
		btnPath.fillColor="#ededed";
		btnPath.strokeColor="#808080";
		btnPath.strokeWidth = 1;
		
		btnPath.shapeName = shapes[i];
		
		var txt = new PointText(new Point(75,12));
		txt.content = shapes[i];
		txt.characterStyle= {
			font:"Courier",
			fontSize:10,
			fillColor:"#000000"
		}
		txt.paragraphStyle = {
			justification:"center"
		};
		bGroup.addChild(btnPath);
		bGroup.addChild(txt);
		bGroup.position.x = xOff;
		bGroup.position.y = yOff;
		bGroup.value = shapes[i];
		buttons.push(btnPath);
		yOff += 23;
	}
	yOff += 23;
	
	var pauseButtonGroup = new Group();
	var pauseButtonRect = new Rectangle(new Point(0,0),new Size(150,20));
	pauseButton = new Path.Rectangle(pauseButtonRect);
	pauseButton.fillColor="#ededed";
	pauseButton.strokeColor="#808080";
	btnPath.strokeWidth = 1;
	var pauseButtonText = new PointText(new Point(75,12));
	pauseButtonText.content = "PLAY/PAUSE";
	pauseButtonText.characterStyle= {
		font:"Courier",
		fontSize:10,
		fillColor:"#000000"
	}
	pauseButtonText.paragraphStyle = {
		justification:"center"
	};
	pauseButtonGroup.addChild(pauseButton);
	pauseButtonGroup.addChild(pauseButtonText);
	pauseButtonGroup.position.x = xOff;
	pauseButtonGroup.position.y = yOff;
}

function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var obj = new Path.Circle(new Point(centerX,centerY),5);
	obj.style={
		fillColor:'#'+randomRGB()+randomRGB()+randomRGB(),
		strokeColor:'#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 onMouseDown(e) {
	for(var i=0;i<buttons.length;i++) {
		if(buttons[i].hitTest(e.point)) {
			currentShape = buttons[i].shapeName;
			isInitialized = false;
			onFrame();
			isInitialized = true;
		}
	}
	if(pauseButton.hitTest(e.point)) {
		isPaused = !isPaused;
	}
}
function onMouseMove(e) {
	for(var i=0;i<buttons.length;i++) {
		if(buttons[i].hitTest(e.point)) {
			buttons[i].fillColor="#ffffff";
		} else {
			buttons[i].fillColor="#ededed";
		}
	}
	if(pauseButton.hitTest(e.point)) {
		pauseButton.fillColor="#ffffff";
	} else {
		pauseButton.fillColor="#ededed";
	}
}

function onFrame(e) {
	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].position.x = newX;
		objects[i].position.y = newY;
	}
	theta += (orbitSpeed*direction);
}

Here is a line-by-line breakdown of the above Paperscript:

  • 2-6 – initialize some global variables. Note that there is a “pauseButton” variable here. This will be important later.
  • 9-33 – instantiating variables for the trigonometry animations. Nothing here is Paper.js specific.
  • 35 – method call to create the user interface
  • 36 – method call to create the circle graphics used in the animation
  • 37 – display the initial pattern
  • 38 – set the state of the app to “initialized”
  • 39-92 – initInterface() – create the button used to control the animations
    • 40-41 – initialize variables for positioning the UI buttons
    • 42-69 – iterate through the shapes array and create a button for each element therein
      • 43 – create a Group object to hold the elements that make up a button
      • 44 – create a Rectangle object
      • 45 – create a display object to display the Rectangle on the stage
      • 46-48 – style the rectangle
      • 50 – associate the name of the associated animation pattern with this button
      • 52 – create and position a new PointText object for displaying text on the stage.
      • 53 – add text to the PointText object
      • 54-58 – style the text in the text object
      • 59-61 – justify the text in the button. This causes the registration point for the object to be centered. See also the x and y values for the text object created on line 52
      • 62-63 – add the gray rectangle and the text to the Group for this button
      • 64-65 – position this button on the stage
      • 66 – associate the name of the animation shape with this button
      • 67 – add this button to the array of button objects
      • 68 – iterate the y position variable for the buttons
    • 70 – add some space between the preceding buttons and the play/pause button
    • 72-91 – create the play/pause button the same way the as the buttons created on lines 43-67
  • 94-98 – initObjects() – create a number of circle graphics equal to the value of the startingObjects variable
  • 99-108 – addObject() – create a circle graphic and add it to the stage
    • 100 – create a new Circle display object
    • 101-104 – color in the circle and give it an outline
    • 105 – add the circle to the objects array
    • 106 – increase the value of numObjects by one
    • 107 – adjust the spacing of the circle graphics in the animation
  • 109-112 – removeObjects() – not used in this demo
  • 114-118 – randomRGB() – create and return a random hexadecimal value between 0 and 255, inclusive.
  • 121-133 – onMouseDown() – listen for the mouseDown event on the stage
    • 122-129 – iterate through the buttons to see if one of them intersected the click event
      • 123 – if a point on a button has intersected the the coordinates of the mouse click…
      • 124 – set the current animation shape to equal the shape associated with that button
      • 125-127 – update the screen to reflect this change (these lines are only needed if the animation is paused)
    • 130-132 – check to see if the play/pause button intersected the click coordinates, and if so, toggle the isPaused boolean value
  • 134-147 – onMouseMove() – listen for mouse move events on the stage
    • 135-141 – iterate through the buttons to see if one of them is currently being moused over
      • 136-137 – if a button is being moused over, set its fill color to white
      • 138-140 – if not, set its fill color to light gray
    • 142-143 – if the pause button is moused over, set its fill color to white.
    • 144-146 – if not, set its fill color to light gray
  • 149-199 – onFrame(e) – built-in Paper.js method to allow frame-based animations
    • 150 – if the animation is paused, do nothing
    • 151-197 – iterate through the circle graphics and update the position of each
      • 152-194 – for each circle, figure out where it should be based on its position within the list of circle objects, and the value of the currentShape variable
      • 195-196 – update the position of the circle graphic on the stage
    • 198 – update the rotation of the animation for the next frame

So there it is: a trigonometric curves explorer app, done up with Paper.js. It takes a little more work to get things on the screen, and event handling is not as simple as some other libraries, but the level of control over the graphics, and the performance compared to Easel, oCanas, and Raphael, more than make up for the difference.

As with the other posts in this series, the code for the various shapes of the animations comes from the Simple Trigonometric Curves Tutorial over at Kongregate.

Enjoy!

 

Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: Paper.js

HTML5 Canvas Drawing Library Exploration: Raphael.js

2012-02-05 John Winkelman

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!

Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: Raphael.js

HTML5 Canvas Drawing Library Exploration: oCanvas.js

2012-01-29 John Winkelman

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.

 

Posted in ProgrammingTagged math, procedural art comment on HTML5 Canvas Drawing Library Exploration: oCanvas.js

Serving Up Static HTML Pages in Drupal Gardens Websites

2012-01-27 John Winkelman

As the learning curve for out-of-the-box content management systems flattens out, hand-coded HTML pages are becoming something of a rare bird. With services like Facebook, WordPress, Tumblr, and a hundred others, there simply isn’t as much call for hand-coded, one-off web sites. However, they still have their place – either in sites like this one, where I like posting experiments in a variety of languages, or for more professional organizations, where HTML wireframes or interactive comps would be more useful than static Photoshop documents and Powerpoint presentations.

Therefore, I took it upon myself to figure out how to serve up individual, custom HTML pages from within the confines of this site, which is a custom installation of Drupal 7, hosted at Drupal Gardens, and served up as cloud-based Software-as-a Service.

It is quite simple, actually. First, you need to have a static HTML page, which either contains within it everything you need, or has links to the appropriate outside assets. These could be images, external stylesheets, Flash movies, or the like. So if you have something like this:

<!DOCTYPE html>
<html>
	<head>
		<title>Static HTML Page Demo</title>
		<style>
			* {margin:0;padding:0;}
			body {background:#ffffff;}
			p {font:12px/18px arial,sans-serif;color:#333333;}
		</style>
		<script type="text/javascript" src="/sites/mysite.drupalgardens.com/files/my-groovy-code.js"></script>
	</head>
	<body>
		<p>Welcome to my wonderful website</p>
	</body>
</html>

…in a file called, say, “static-html-page-example.html”. Then, in order to put it in a place where it is accessible to the world, you can do the following:

  1. Log into your Drupal Gardens website
  2. Click on “Configuration”
  3. In the “Media” box of the configuration page, click on “Media browser settings”
  4. In the “Allowed file extensions” text area, add html to the end. Be sure to use a space to separate it from the other extensions.
  5. Click “Save configuration”.
  6. In the upper bar of the content area, click on “content”
  7. When the main content page opens, click on the “MEDIA” tab on the right side of the page
  8. In the new screen, click “+ Add File”
  9. drag your HTML file onto the “add file” area (or browse to it and click “add”), then click “Start Upload”
  10. You will now see your file appear in the list of media assets for your library. Click on it.
  11. You should now see a page, the content of which is a link to your static HTML page. If you click that link, it will take you to that page. Click here to see how it works.

See? That was easy! Once you have your assets created, all you need to do is organize and upload.

You will only need to performs steps 1-5 once. After you add a file extension, it is there for good.

Since Drupal Gardens has constraints over and above a normal Drupal install, some allowances must be made. Here are a few things to watch out for:

  • Every external asset in your static  page – images, videos, Flash files, style sheets, etc., must also be uploaded to the Drupal Gardens media directory. The same process you used to serve up your static HTML file also applies for these assets. You shouldn’t need to add file extensions for images or videos, but you might for Flash (swf), and you will for Javascript (js) and style sheets (css).
  • Since every asset is served from the files directory, you need to make sure you acount for this when you create the HTML page. The path to all of you assets will be
    “/sites/mysite.drupalgardens.com/files/myAsset.css”. Be sure to add the leading slash.

    • Alternately, you could add a <base href=”/sites/mysite.drupalgardens.com/files/”/> element in the <head/> element of your static HTML file (information here). This would cut down on the amount of modification you would need to do when moving the static site from your computer to Drupal Gardens. You could simple add the tag to all of your HTML files right before you upload everything.
  • Again, since all uploaded media, files, assets, etc. for your entire Drupal Gardens site are stored in a single directory, it is important to name all of your assets in such a way that it is easy to determine which asset goes with which project. I have found that using a prefix is a great way to keep things organized. Here are a couple of examples:
    • “20120126-index.html”
    • “20120126-image-0.jpg”
    • “20120126-stylesheet.css”
    • “clientname-index.html”
    • “date-client-file-1.html”

There! The best of both worlds: A managed Drupal environment, and the ability to upload and serve custom, static HTML pages if and as the need arises.

Posted in Programming comment on Serving Up Static HTML Pages in Drupal Gardens Websites

HTML5 Canvas Drawing Library Exploration: Easel.js

2012-01-26 John Winkelman

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!

Posted in ProgrammingTagged math comment on HTML5 Canvas Drawing Library Exploration: Easel.js

Accessing Views From Flash in Drupal Gardens

2011-12-19 John Winkelman

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.

Posted in Programming comment on Accessing Views From Flash in Drupal Gardens

Procedural Generation X – User Modified Content

2011-11-14 John Winkelman

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

 

Posted in ProgrammingTagged game development, procedural art comment on Procedural Generation X – User Modified Content

Away3d and Flash Player 11 – Source Code

2011-11-06 John Winkelman

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.

Posted in ProgrammingTagged Flash, procedural art comment on Away3d and Flash Player 11 – Source Code

Away3d and Flash Player 11

2011-11-03 John Winkelman

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.

Posted in ProgrammingTagged Flash, procedural art comment on Away3d and Flash Player 11

Posts navigation

Older posts
Newer posts

Personal website of
John Winkelman

John Winkelman in closeup

Archives

Categories

Posts By Month

May 2025
S M T W T F S
 123
45678910
11121314151617
18192021222324
25262728293031
« Apr    

Links of Note

Reading, Writing
Tor.com
Locus Online
The Believer
File 770
IWSG

Watching, Listening
Writing Excuses Podcast
Our Opinions Are Correct
The Naropa Poetics Audio Archive

News, Politics, Economics
Naked Capitalism
Crooked Timber

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

© 2025 Ecce Signum

Proudly powered by WordPress | Theme: x-blog by wpthemespace.com