A Simple Javascript Canvas Render Framework

A Simple Javscript Canvas Render Framework

It is possible to do a great number of things in many different languages, and due to the popularity of the some of the particular features of javascript, it is also possible to do these in javascript. The trick is to figure out what is needed in terms of state, and logic, and provide a looping mechanism to make things happen on a regular basis. The particular thing that I am talking about here is rendering a frame – or essentially drawing a picture.

Building the framework

There are some skills that we need to borrow, or have in our tool belt in order to build an actual framework, which essentially boils down to creating a namespace for ourself so that we don’t clutter the global one. We will add all of our functions, objects, properties and variables to this namespace so that we can organize them into a functional unit.


var SwhistleSoft = SwhistleSoft || {
	implement:function(name,implementation){
		parent = window;
		parent["SwhistleSoft"][name] = implementation ;
	 }
};

The monstrosity that has just been spewed out to you is the code that sets it all up. What we are essentially doing in the above example is providing an implement function which allows us to attach objects to our “SwhistleSoft” object via a name passed in as the first variable and the implementation or object as the second. It is a feature of the javascript language to allow us to treat an object as an array and attach properties in this fashion.

An Simple example object ( Vector )

Now that we have a standardized way to attach things to our named space, lets have an example. One of the “classes” or “objects” that we will need is a vector class that essentially stores a magnitude and direction as two integers – x and y.

SwhistleSoft.implement ( "vector",function(x,y){
	var x = x ;
	var y = y ;
	this.X = function(){
		return x ;
	},
	this.Y = function(){
		return y ;
	}
	this.Update=function(newx,newy){
		x = newx ;
		y = newy ;
	}
});

What we are doing here is calling the implement function of our global SwhistleSoft variable/object which essentially attaches the vector named function/class object to the global SwhistleSoft variable/object. This will allow us to construct a vector, then update the value and return the values as functions. Please also note that since the var x and var y are internal variables – they should be hidden from the external scope.

Instantiating a vector
The following example creates a new vector object using a “constant” multiplied by the sin and cos of an angle to get the opposite and adjacent sides of the triangle the angle it would form based on a magnitude that is multiplied.

vctr=new SwhistleSoft.vector(vMag*Math.cos(vAngle),vMag*Math.sin(vAngle));

Returning the value of the vector
The following example simply returns the vectors current x and y values and associates them with the appropriately named local context x and y variable.

x += vctr.X() ;
y += vctr.Y() ;

Updating the vector
The following example simply updates the vector x and y values with some new values based on the current value and another vector called g(gravity in this case)

vctr.Update(vctr.X()*.999 + g.X(),(vctr.Y()*.999)+g.Y());

Now that we have seen how to use the framework implement function, lets get to building some other helper classes:


Loader
The loader essentially attempts to provide a cross platform document ready type handler that we can call and attach many function/event handlers to. For the webkit/gecko browssers we have DOMContentLoaded, and for the IE world we have defer javascript injection. And finally we have a fallback window.onload that has been overwritten with our new function while the original is preserved. Because we maintain a list of functions here ( loadfuncs ) it is possible to iterate through that list and call all of the functions in a seq instead of a one-off only overwrite scenario.

SwhistleSoft.implement("loader",new function(){
		var loadfuncs = [];
		var oldonload = window.onload ;
		var alreadyrunflag=0 //flag to indicate whether target function has already been run
		if (document.addEventListener){
			document.addEventListener("DOMContentLoaded", function(){alreadyrunflag=1; SwhistleSoft.loader.exec();}, false)
		}else if (document.all && !window.opera){
			document.write('<script type="text/javascript" id="contentloadtag" defer="defer" src="javascript:void(0)"><\/script>')
			var contentloadtag=document.getElementById("contentloadtag")
			contentloadtag.onreadystatechange=function(){
				if (this.readyState=="complete"){
					alreadyrunflag=1
					SwhistleSoft.loader.exec();
				}
			}
		}
		window.onload=function(){
			setTimeout(function(){if (!alreadyrunflag){SwhistleSoft.loader.exec();}}, 0);
			if ( typeof ( oldonload ) == "function" ){
				oldonload();
			}
		};
		return{
			exec:function(){
				for ( i = 0 ; i < loadfuncs.length ; i++ ){
					loadfuncs[i]();
				}
				return true ;
			},
			add :function(fnc){
				loadfuncs.push(fnc);
				return true ;
			}
		}
});


Particle
The following particle object maintains state of the old or last position, as well as color, vector, and gravity. When we update the particle, we decrease the vector/velocity power and add gravity to it providing a sort of deceleration and eventual decay. The update function checks to see if we are still on screen and need to maintain or regenerate the particle at a new random velocity and magnitude/direction. The reset function essentially is in effect generating the “new particle” by reusing the current variable and assigning new properties.

SwhistleSoft.implement ( "particle",function(x,y,clr,newvctr){
	var x = x ;
	var y = y ;
	var ox = x ;
	var oy = y ;
	var clr = clr ;
	var vctr = newvctr ;
	var g = new SwhistleSoft.vector(0,0.1);
	this.Update = function(range){
		x += vctr.X() ;
		y += vctr.Y() ;
		vctr.Update(vctr.X()*.999 + g.X(),(vctr.Y()*.999)+g.Y());
		if ( !range.InRange(x,y)){
			this.Reset();
		}
	}
	this.X = function(){
		return x ;
	} ;
	this.Y = function(){
		return y ;
	}
	this.Color = function(){
		return clr ;
	}
	this.Vector = function(){
		return vctr;
	}
	this.Reset = function(){
		x =ox ;
		y = oy ;
		vAngle = Math.random()*2*Math.PI;
		vMag = 6*(0.6 + 0.4*Math.random());
		vctr=new SwhistleSoft.vector(vMag*Math.cos(vAngle),vMag*Math.sin(vAngle));
		g = new SwhistleSoft.vector(0,0.1);
	}
});


Range
The range object as you may be able to see mirrors a Rectangle object in the .net Draw namespace implementing left, top, right and bottom ( width/height ). The InRange function implements a check to see if a given point is inside the range.

SwhistleSoft.implement ( "range",function(left, top, right, bottom){
	var l = left;
	var t  = top;
	var r = right ;
	var b = bottom;
	this.Left = function(){ return l ; }
	this.Top = function(){ return t ; }
	this.Right = function(){ return r ; }
	this.Bottom = function(){ return b ; }
	this.InRange = function(x,y){
		if ( x < l || x > r ||  y < t || y > b  ){
			return false ;
		}
		return true ;
	}
});


Canvas
The canvas object ties in all of the preceeding objects, and sets up some additional variables to provide a sort of fireworks looking display. A lot of this code is based on a similar work which does not implement the framework container, and a couple of itsy bitsy features. The Run loop is the magic of this system which sets up a continual loop to update the system using setInterval with 1000/30 milliseconds or essentially 1 30th of a second. This is not going to happen in reality because processing will need to be done in order to keep the system running, but it is a fair approximation. MakeSpriteSheet sets up some programmatic graphics, and fade uses an “offscreen” surface or bitmap to fade the current bits to 0 or until black. A different approach to using a black would be to compare with an existing background image that is preserved and to not decay that color any more.

SwhistleSoft.implement ( "canvas", function(id,newparticleCount){
	var displayWidth=0;
	var displayHeight = 0;
	var center = {X:function(){},Y:function(){}};
	var particleCount = 0;
	var cvs = null ;
	var context = null ;
	var particles = [];
	var redFade = 4;
	var greenFade = 9;
	var blueFade = 32;
	var r ;
	var g ;
	var b ;
	var range ;
	
	
	var spriteSheetCanvas;
	var spriteSheetContext;
	
	
	var particleColor = "#FFFFFF";//"rgba(64,200,255,1)";
	var particleRad = 16;
	var coreRad = 5;
	var haloMaxAlpha = 0.33;
	var particleDiam = 2*particleRad;
	
	var boundaryRad = 0.5*Math.min(displayWidth,displayHeight) - particleRad - 1;
	var boundaryRadSquare = boundaryRad*boundaryRad;
	
	
	
	var coreRad,haloMaxAlpha;
	if ( !cvs ){
		cvs = document.getElementById(id);
		if ( cvs ){
			displayWidth = cvs.getAttribute("width");
			displayHeight = cvs.getAttribute("height");
			context = cvs.getContext("2d");
			context.fillStyle = "#000000";
			context.fillRect(0,0,displayWidth,displayHeight);
			center = new SwhistleSoft.vector(displayWidth/2,displayHeight/2);
			range = new SwhistleSoft.range (0,0,displayWidth,displayHeight);
			particleCount = newparticleCount ;
		}
	}
	this.run=function(cnvs){
		cnvs.update(cnvs);
		cnvs.fade();
		setTimeout(function(){cnvs.run(cnvs);}, 1000/30 );
	}
	this.Particles = function(){
		return particles;
	}
	this.Center = function(){
		return center ;
	}
	this.Range = function(){
		return range ;
	}
	this.update = function(cnvs){
		var particles = cnvs.Particles();
		cnvs.genparticles(1);
		for( i = 0 ; i < particles.length ; i++ ){
			var p  = particles[i];
			p.Update(cnvs.Range());
			context.drawImage(spriteSheetCanvas,0,0,particleDiam,particleDiam,Math.round( p.X() - particleRad),Math.round( p.Y() - particleRad),particleDiam,particleDiam);
		}
	}
	this.genparticles=function(amount){
		if ( particles.length < particleCount ){
			var vAngle;
			var vMag;
			for ( i = 0 ; i < Math.min(particleCount,amount);i++){
				vAngle = Math.random()*2*Math.PI;
				vMag = 4*(0.6 + 0.4*Math.random());
				particles.push(new SwhistleSoft.particle(center.X(),center.Y(),0x000000,new SwhistleSoft.vector(vMag*Math.cos(vAngle),vMag*Math.sin(vAngle))));
			}
		}
	}
	this.makeSpriteSheet =  function () {
		spriteSheetCanvas = document.createElement('canvas');
		spriteSheetCanvas.width = particleRad*2 ;
		spriteSheetCanvas.height = particleRad*2 ;
		spriteSheetContext = spriteSheetCanvas.getContext("2d");
		//draw
		var edgeColor = "rgba(255,255,255,"+haloMaxAlpha+")";
		var outerColor = "rgba(255,255,255,0)";
		var grad = spriteSheetContext.createRadialGradient(particleRad, particleRad, 0, particleRad, particleRad, particleRad);
		grad.addColorStop(0,edgeColor);
		grad.addColorStop(coreRad/particleRad,edgeColor);
		grad.addColorStop(1,outerColor);
		spriteSheetContext.fillStyle = grad;
		spriteSheetContext.beginPath();
		spriteSheetContext.arc(particleRad, particleRad, particleRad, 0, 2*Math.PI, false);
		spriteSheetContext.closePath();
		spriteSheetContext.fill();
		
		spriteSheetContext.fillStyle = particleColor;
		spriteSheetContext.beginPath();
		spriteSheetContext.arc(particleRad, particleRad, coreRad, 0, 2*Math.PI, false);
		spriteSheetContext.closePath();
		spriteSheetContext.fill();
		
		return true ;
	}

	this.fade=function(){
		var lastImage = context.getImageData(0,0,displayWidth,displayHeight);
		pixelData = lastImage.data;
		len = pixelData.length;
		for (i=0; i<len; i += 4) {
			if ((r = pixelData[i]) != 0) {
				r -= redFade;
				g = pixelData[i+1]-greenFade;
				b = pixelData[i+2]-blueFade;
				pixelData[i] = (r < 0) ? 0 : r;
				pixelData[i+1] = (g < 0) ? 0 : g;
				pixelData[i+2] = (b < 0) ? 0 : b;
			}
		}
		context.putImageData(lastImage,0,0);
	}
} ) ;


Setting up the script from the main html
The following code is all that is needed to insert a document ready function in order to be able to run the example. The only thing that remains to be included is the canvas with id canvs.

SwhistleSoft.loader.add ( function(){
	cnvs = new SwhistleSoft.canvas("canvs",20);
	cnvs.genparticles(3);
	cnvs.makeSpriteSheet();
	cnvs.run(cnvs);
});