HTML5 Canvas Drawing Library Exploration: Processing.js

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.