Rock, Paper, Scissors

Rendering to Canvas

Our final file, core.js, manages our rendering, user input, etc. We begin by declaring a number of global variables, including a reference to the Canvas drawing context (ctx), the dimensions we'll draw each map tile & cell, (tileW, tileH), the dimensions of the Canvas we're drawing on (canvasW, canvasH), and variables for calculating framerate & the update rate when the animation is not paused.


var ctx = null;

var tileW = 20, tileH = 20;
var canvasW = 400, canvasH = 400;

var currentSecond = 0, frameCount = 0, framesLastSecond = 0;
var gameTime = 0, lastFrame = 0, lastUpdate = 0, updateDelay = 2000;

We'll also use some global flags to see if the animation is paused, if we want to run a step of the animation on the next frame update, and if we want to reset the animation completely on the next frame:


var paused		= false;
var stepFrame	= false;
var reset		= false;

We also want to build a list of types the user can create, and the index in the list which can currently be created by the user:


var creatableTypes = [];
var createType = 0;

Our final global keeps track of the mouse state; whether or not it is over the canvas element, its position, and the point on the canvas the mouse was last clicked on (or null if there is no unprocessed click):


var mouseState = {
	over : false,
	x : 0,
	y : 0,
	click : null
};

Our onload function will begin by setting the canvasW, canvasH based on the actual dimensions of our webpages canvas. We then create a keypress even handler, which, depending on the key pressed, will cycle through the current type the cell type the user creates by clicking on the map (c), toggles the paused state on or off (p), tells the animation we want to animate a single step if the animation is currently paused (s), if we want to toggle a complete system reset (r), toggling directional biasing on or off (b), changing the speed of the automatic update rate (d), and filling in the whole map with random Cells (f):


	document.addEventListener('keypress', function(e) {
		if(String.fromCharCode(e.which).toLowerCase()=='c')
		{
			createType++;
			if(createType>=creatableTypes.length) { createType = 0; }
		}
		else if(String.fromCharCode(e.which).toLowerCase()=='p') { paused = !paused; }
		else if(String.fromCharCode(e.which).toLowerCase()=='s' && paused) { stepFrame = true; }
		else if(String.fromCharCode(e.which).toLowerCase()=='r') { reset = true; }
		else if(String.fromCharCode(e.which).toLowerCase()=='b') { biasDir = !biasDir; }
		else if(String.fromCharCode(e.which).toLowerCase()=='d')
		{
			updateDelay+= 200;
			if(updateDelay>=2000) { updateDelay = 200; }
		}
		else if(String.fromCharCode(e.which).toLowerCase()=='f')
		{
			for(var x in Map.tiles)
			{
				if(Map.tiles[x].cell!=null) { continue; }
				
				var t = Math.floor(Math.random() * creatableTypes.length);
				new Cell(Map.tiles[x].x, Map.tiles[x].y, creatableTypes[t], cellTypes[creatableTypes[t]].prey);
			}
		}
	});

We also create a number of mouse event handlers to update the mouseState global depending on the action performed:


	document.getElementById('map').addEventListener('mouseout', function() {
		mouseState.over = false;
	});
	document.getElementById('map').addEventListener('click', function(e) {
		if(e.which==1)
		{
			mouseState.click = [mouseState.x, mouseState.y];
		}
	});
	document.getElementById('map').addEventListener('mousemove', function(e) {
		// Get the position of the mouse on the page
		var mouseX = e.pageX;
		var mouseY = e.pageY;

		// Find the offset of the Canvas relative to the document top, left,
		// and modify the mouse position to account for this
		var p = document.getElementById('map');
		do
		{
			mouseX-= p.offsetLeft;
			mouseY-= p.offsetTop;

			p = p.offsetParent;
		} while(p!=null);
		
		mouseState.over = true;
		mouseState.x = mouseX;
		mouseState.y = mouseY;
	});

The final purposes of our onload function will be to assign the 2D drawing context of the Canvas element to the ctx global, generate a new empty map, fill our creatableTypes global from the list of cellTypes, and then to let the window know it should call our updateMap method when it is ready to render to the Canvas:


	ctx = document.getElementById('map').getContext('2d');
	
	Map.generate(28, 18);
	
	for(x in cellTypes) { creatableTypes.push(x); }
	
	requestAnimationFrame(updateMap);
};

Finally, we create a single function, updateMap, which handles our general drawing, update processing, and handling flagged events such as resetting and stepping the animation. First of all, this function updates timers to keep track of update events and framerate:


function updateMap()
{
	if(ctx==null) { return; }
	
	// Framerate & game time calculations
	var sec = Math.floor(Date.now()/1000);
	if(sec!=currentSecond)
	{
		currentSecond = sec;
		framesLastSecond = frameCount;
		frameCount = 1;
	}
	else { frameCount++; }
	
	var now = Date.now();
	gameTime+= (now-lastFrame);
	lastFrame = now;

Next, if the reset event has been toggled, or if the animation should proceed another step due to the step event being toggled or because enough time has elapsed since the last update, these events are processed accordingly:


	if(reset)
	{
		reset = false;
		
		allCells.splice(0, allCells.length);
		newCells.splice(0, newCells.length);
		Map.generate(Map.width, Map.height);
	}
	if((paused && stepFrame) || (!paused && updateDelay!=0 && (gameTime-lastUpdate)>=updateDelay))
	{
		updateCells();
		lastUpdate = gameTime;
		stepFrame = false;
	}

Additionally, if the mouse has been clicked on the Canvas, and the click event falls on a map tile, check the tile is free of occupying cells; if so, create a new cell here of the currently selected type for creation:


	// If there's been a click, attempt to create a new
	// cell at the given location:
	if(mouseState.click)
	{
		var x = Math.floor(mouseState.click[0] / tileW);
		var y = Math.floor(mouseState.click[1] / tileH);
		
		if(x < Map.width && y < Map.height && Map.tiles[((y*Map.width)+x)].cell==null)
		{
			new Cell(x, y, creatableTypes[createType], cellTypes[creatableTypes[createType]].prey);
		}
		
		mouseState.click = null;
	}

We can then begin rendering to our Canvas. We'll start by setting the font, clearing the canvas to a grey, and drawing the map grid:


	ctx.font = "bold 10pt sans-serif";
	ctx.fillStyle = "#cccccc"
	ctx.fillRect(0, 0, canvasW, canvasH);
	
	// Draw grid
	ctx.strokeStyle = "#ffffff";
	
	ctx.beginPath();
	for(var y = 0; y <= Map.height; y++)
	{
		ctx.moveTo(0, (y * tileH));
		ctx.lineTo((Map.width * tileW), (y * tileH));
	}
	for(var x = 0; x <= Map.width; x++)
	{
		ctx.moveTo((x * tileW), 0);
		ctx.lineTo((x * tileW), (Map.height * tileH));
	}
	ctx.stroke();

We'll then draw the cells. Active cells are drawn as a filled block of colour depending on the cells type, whilst newly created cells are drawn as an outlined rectangle:


	for(var x in allCells)
	{
		if(!allCells[x].alive) { continue; }
		
		ctx.fillStyle = cellTypes[allCells[x].type].colour;
		ctx.fillRect( (allCells[x].x * tileW), (allCells[x].y * tileH), tileW, tileH);
	}
	
	for(var x in newCells)
	{
		ctx.strokeStyle = cellTypes[newCells[x].type].colour;
		ctx.strokeRect( (newCells[x].x * tileW), (newCells[x].y * tileH), tileW, tileH);
	}

We'll also draw some text showing the current framerate, animation status, and giving instructions on how to toggle and change settings in the animation:


	ctx.fillStyle = "#000000";
	ctx.textAlign = "end";
	ctx.fillText("Framerate: " + framesLastSecond +
		(paused ? " (Paused)" : ""), canvasW-50, 20);
	ctx.fillText("Creating: " + creatableTypes[createType] + " (c to change)", canvasW - 50, 35);
	
	ctx.textAlign = "start";
	ctx.fillText("Bias direction: " + (biasDir ? "On" : "Off") + " (b to change)", 10, canvasH - 45);
	ctx.fillText("s to Step when paused, r to reset, f to random fill", 10, canvasH - 30);
	ctx.fillText("Auto step time: " + updateDelay + "ms (d to change)", 10, canvasH - 15);

Finally, we'll let the browser know that when it's ready to animate another frame, it needs to call this function once again:


	requestAnimationFrame(updateMap);
}
Page loaded in 0.01 second(s).