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.
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
- This method initializes the global variables.
- 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
- This method creates all of the user control elements, styles them and adds text, and assigns mouse event listeners
- 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
- This method creates a circle and adds it to the display list
- 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
- 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
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!