HTML5 Canvas Drawing Library Exploration: Raphael.js

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

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

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

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

Click here to launch the demo.

HTML

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

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

Javascript

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

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

onload = init;

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

function initObjects() {
	for(var i=0;i<startingObjects;i++) {
		addObject();
	}
}
function addObject() {
	var obj = canvas.circle(centerX,centerY,5);
	obj.attr({
		fill:'#'+randomRGB()+randomRGB()+randomRGB(),
		stroke:'#000000'
	});
	objects.push(obj);
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}
function removeObject() {
	numObjects = objects.length;
	objectInterval = orbitSteps/numObjects;
}

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

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

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

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

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

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