Page 1 of 1

HTML5 - Canvas Collaboration Rate Topic: -----

#1 modi123_1  Icon User is online

  • Suitor #2
  • member icon



Reputation: 8938
  • View blog
  • Posts: 33,504
  • Joined: 12-June 08

Posted 15 December 2012 - 09:14 PM

Topics covered:
  • HTML5 Canvas
  • Javascript events
  • PHP server scripts
  • AJAX calls w/ jQuery
  • MYSQL tables

Tools Required:
  • an HTML5 compliant browser
  • your development IDE of choice (I am using Eclipse)
  • MYSQL active and hosted
  • PHP Version 5.3.1 or greater
  • Apache to run the MYSQL and PHP (I am using XAMPP)


In my previous tutorial I covered the basics of drawing on an HTML5 canvas - locally. With this tutorial I am going to expand that concept to share the drawings across multiple instances of people on the same page. Now there are various ways to go about this, but the interesting option I choose was to send the coordinates, drawn locally, to a MYSQL table where other clients would poll for new data points. If new points were found it would draw them on the local client.

I will not be covering the basics of my past tutorial - so it would be advised you checked it out first!

I am going to assume you are moderately familiar with PHP syntax and MYSQL table creations.

This setup also allows for easy flexibility in bolting on new functionality. The tables are setup in away to allowed timed fading of old drawn points, an admin panel, various commands to be distributed, and tracking by the host. It also is a means of transmitting past data to new users who sign on. Over all a solid option.

Big Picture
Conceptually here's what I have going down.

Everything is going to be tied together with a client id. From the load, timers, drawing, and unloading of the HTML page.

The client id is going to help limit who can and cannot draw, cull recent points from other users, and also help send commands to other folks as well.

When the user opens up this page the javascript is going to attempt to get a client id from the mysql table. The PHP is going to see how many people are logged in and if there is already too many do not allow the user to draw on their canvas (and by extension add to the collective pot of drawing points).

If the user does not secure an id the timer interval is set to check for an open slot (and grab it if it is open) and also redraw all the points in the collective pot.

Finally when the page is closed or refreshed an event will fire off to remove the id from the table - thus freeing up a spot for another user.

Depending on what you are transmitting - limiting who can draw ( aka saving coordinates in the table) will help limit and balance data transfers on your host's bandwidth. A nice touch, right?

The local drawing function from the previous tutorial adds a line to send the client's coordinates to the communal pot for all the other users to grab. This also sends width and coloring (important parts for other canvases to know when drawing). As I mentioned above - sending some of this information allows for neat tricks like the fading of the color on coordinates after a specific time, or to highlight one user's lines instead of another.

Finally the clear command has been expanded to a table where other clients can pickup when clear was issued and act upon that before drawing the current set of points.

Not too terribly difficult, right?

The layout looks like this:
Attached Image

MYSQL Tables:

blackboard_client

Attached Image

blackboard_commands

Attached Image

blackboard_points

Attached Image

Javascript
Okay let's get into the code.

Initializing - In addition to the previous mundane variables - I will need a client id variable to hold what is picked up. I am also going to need a 'last row' id to help minimize how many rows of data I am getting back.

// -- Variables
var bPenDown = false; // determine if the pen is down to write or not.
var rgbLineColor = "FFFFFF"; // default color is white - this could be

var lLineWidth = 1; // the line width holder

// the context and canvas of our html5 canvas object
var oCanvas;
var oContext;

// the point array to track the pen while it is down.
var myPen =
{};

// everyone who opens the page must have an client id -
// so we can tell which points were drawn by us or someone else and only draw
// everyone else's points.
var clientid = 0;

var lastRowID = 0; // to minimize getting all the data back a small check says
// only get the points that are new.
// -- init - after form load

window.onload = init;// just load the initialization when the form loads.



In our init we are also going to add our timer hook up. This will have the page hit the 'clock' method every one second to grab updated line segments from other users as well as checking to see if we can draw (assuming we cannot right now).
// initialize our canvas and all the events we will need.
function init()
{
	// get the canvas object
	oCanvas = document.getElementById("myCanvas");
	// set the context to plain 2d dreawing
	oContext = oCanvas.getContext("2d");
	// setup the fill style to the default (white)
	oContext.fillStyle = "#" + rgbLineColor;
	// set the default selector for the pen
	document.getElementById("penWidth").selectedIndex = 1;
	// set the initial pen width
	lLineWidth = 3;
	// register the rest of the events.
	registerEvents();
	// try and get an id
	Get_ID();
	// set the clock to get any new coordinates every one second.
	setInterval(clock, 1000);
}



When we are registering events we need to add on for when the page is unloaded. This will ensure a client id slot is opened for someone else to use!

// the main events needed to be listened for
function registerEvents()
{
	// mouse evens.
	oCanvas.onmousedown = mouseDownListener;
	oCanvas.onmousemove = mouseMoveListener;
	oCanvas.onmouseup = mouseUpListener;
	// when the form refreshes or quits clear out that client id.
	window.onbeforeunload = onunload;
	// if the user wants a new pen make sure to change the width.
	document.getElementById("penWidth").onchange = penWidthListener;
}



The mouse events stay pretty similar with the exceptions of short circuiting a few if we cannot draw (the mouse down and mouse move)...

/*
 * ============================== EVENTS ========================
 */

// When the mouse is clicked in the canvas get teh coordinates it is clicked and
// set the boolean for the mouse is down
function mouseDownListener(e)
{
	// if there is no client id (aka too many clients already logged in) do not
	// allow drawing.
	if (clientid < 1)
	{
		return;
	}
	// grab the event object which contains our mouse locations.
	var event = e || window.event;// IE -> window.event, not e

	// depeneding on where this canvas is we need to do a bit of math to get the
	// right coordinates
	var tempRect = oCanvas.getBoundingClientRect();
	var mouseX = event.clientX - tempRect.left;
	var mouseY = event.clientY - tempRect.top;

	// send those coordinates to the the pen down and track'm
	penDown(mouseX, mouseY);
}

// When the mouse moves track that too!
function mouseMoveListener(e)
{
	// if there is no client id (aka too many clients already logged in) do not
	// allow drawing.
	if (clientid < 1)
	{
		return;
	}

	var event = e || window.event; // IE -> window.event, not e
	// depeneding on where this canvas is we need to do a bit of math to get the
	// right coordinates
	var rect = oCanvas.getBoundingClientRect();
	var mouseX = event.clientX - rect.left;
	var mouseY = event.clientY - rect.top;

	// draw a line with these two end coordiantes.
	penMove(mouseX, mouseY);
}

// When the mouse button is released
function mouseUpListener(e)
{
	// tell the drawing and pen boolean that we are done drawing.
	penUp();
}

// when the pen is down start drawing save those coordinates as the start of the
// line
function penDown(x, y)
{
	bPenDown = true;
	myPen.x = x;
	myPen.y = y;
}


// flips the boolean so we are not drawing lines when the mouse moves.
function penUp()
{
	bPenDown = false;
}

// takes the color, width, and two coordinates to draw a small line on the
// canvas.
function drawLine(color, thickness, x1, y1, x2, y2)
{
	// we are storing the color in the database so add the hash mark.
	oContext.strokeStyle = "#" + color;
	oContext.lineWidth = thickness;

	// it's all about small paths
	oContext.beginPath();
	oContext.moveTo(x1, y1);
	oContext.lineTo(x2, y2);
	oContext.stroke();
}
// when the user selects a different pen - update our size.
function penWidthListener(e)
{
	lLineWidth = this.options[this.selectedIndex].value;
}



The more interesting is the call out to sending the mouse movements to the table. Here's the simple call to shuffle out what we have drawn to the table.

// When the pen moves these coordinates are the 'end point' of the line we want
// to draw.
function penMove(x, y)
{
	// only do this if the pen is down.
	if (bPenDown)
	{
		// send the coordinates to the server so everyone else has them
		SendCoord_New(clientid, myPen.x, myPen.y, x, y, lLineWidth,
				rgbLineColor);

		// Actually draw the line on our screen.
		drawLine(rgbLineColor, lLineWidth, myPen.x, myPen.y, x, y);

		// Shift the last points in as the start of the new line line
		myPen.x = x;
		myPen.y = y;
	}
}



The clear gets an upgrade to send the command out to the server to hold for others to pickup.
// clears the screen of the canvas and clears the coordinates from the table.
function Clear_Down(bClearServer)
{
	oContext.clearRect(0, 0, oCanvas.width, oCanvas.height);
	if (bClearServer = true)
	{
		Clear_Coords();
	}
}



On of the newer event method is the 'unload'. This uses jquery's ajax call to send out the id to the php page responsible for removing the client id.

// the user leaves the page so remove their id in case someone else is in line
// to draw.
function onunload()
{
	// if we do not have an id the don't bother clearing it from the table.
	if (clientid < 1)
	{
		return;
	}

	// http://api.jquery.com/jQuery.ajax/
	// call our 'remove id' php file to take our saved id and drop it from the
	// table.
	$.ajax(
	{
		type : "POST",
		url : "remove_id.php",
		data :
		{
			"clientid" : clientid
		},
		dataType : "json",
		success : function(data)
		{
			// do nothing -> the page is closing!
		}
	});
}




The Clock function does as expected - if we don't have an id check for one, and always get the data.

// when the internal clicks over (and I am not a registered client yet) see if I
// can draw and also get all the latest line updates from the other users.
function clock()
{
	if (clientid < 1)
	{
		Get_ID();
	}
	Get_Data();
}



The sending of the coordinates took a bit of a different route. I could have continued using jQuery's ajax calls, but in this case I wanted to stretch out a bit and use the XMLHTTPRequest.


// this sends the small line drawn locally (with width and color) to the data
// table so everyone else knows it was drawn.
function SendCoord_New(clientid, x, y, x1, y1, width, color)
{
	// http://www.w3schools.com/ajax/ajax_xmlhttprequest_create.asp
	if (window.XMLHttpRequest)
	{
		xmlhttp = new XMLHttpRequest();
	} else
	{// code for IE6, IE5
		xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
	}
	// a quiet transfer of data to php without having to reload the page.
	xmlhttp.open("GET", "set_coords.php?clientid=" + clientid + "&x=" + x
			+ "&y=" + y + "&x1=" + x1 + "&y1=" + y1 + "&width=" + width
			+ "&color=" + color + "", true);
	xmlhttp.send();
}



With getting the data there was some extra tricky manipulations I needed to do and went the jquery route. It's interesting to note that the canvas isn't locked when I am drawing on my local canvas and when I am force updating the other clients' data.

// get any data that other clients have drawn (that is new and not ours)
function Get_Data()
{
	// http://api.jquery.com/jQuery.ajax/
	$.ajax(
	{
		type : "POST",
		url : "get_coords.php",
		data :
		{
			"clientid" : clientid,
			"lastRow" : lastRowID
		},
		dataType : "json",
		success : function(data)
		{
			// if someone issued a clear - clear our local canvas and then draw
			if (data[0] == true)
			{
				Clear_Down(false);
			}

			var foo = data[1];

			if (jQuery.isEmptyObject(data[1]))
			{
				//document.getElementById("debug").innerHTML = "empty";

			} else
			{
				//document.getElementById("debug").innerHTML = "NOT empty";
				// save the last row id grabbed
				lastRowID = foo[foo.length - 1].ID;
			}

			if (foo.length > 0) // if more rows draw'em.
			{
				for ( var i = 0, len = foo.length; i < len; i++)
				{
					oContext.strokeStyle = "#" + foo[i].COLOR;
					oContext.lineWidth = foo[i].WIDTH;

					oContext.beginPath();
					oContext.moveTo(foo[i].X1, foo[i].Y1);
					oContext.lineTo(foo[i].X2, foo[i].Y2);
					oContext.stroke();

				}
			}

		}
	});
}



The get id and the clear coordinates are just repeat examples of the two above.
// grab an id from the table.. if we don't have an id try and get one. Also
// track their browser (for fun)
function Get_ID()
{
	// http://api.jquery.com/jQuery.ajax/
	$.ajax(
	{
		type : "POST",
		url : "get_id.php",
		data :
		{
			"clientid" : clientid,
			"version" : navigator.userAgent
		},
		dataType : "json",
		success : function(data)
		{
			clientid = data;
		}
	});
}
// send a 'clear coordinates' command to the server and drop all the stored
// points.
function Clear_Coords()
{
	if (window.XMLHttpRequest)
	{
		xmlhttp = new XMLHttpRequest();
	} else
	{// code for IE6, IE5
		xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
	}
	xmlhttp.open("GET", "clear.php?clientid=" + clientid, true);

	xmlhttp.send();
}




PHP
The PHP methods are pretty similar in their form and function. A PDO connection, a basic CRUD (create, request, update, delete) SQL string, and pushing back the values in a JSON question for the AJAX/jquery to handle.

The more complex and interesting one was the 'get coordinates'. This not only had to get all the recent drawn points but also determine if a clear happened. Sure it's a bit ducttape'ish with checking the insert time of the command against the current time, but it works pretty well.

<?php
// get relevant links.
$clientid = $_POST["clientid"];
$lastRow = $_POST["lastRow"];

// db connection
$dbh = new PDO('mysql:host=localhost;dbname=test', '123', '');
//
$rowClear = false;
try
{
	// check the last time there was a clear..
	$stmt = $dbh->prepare('SELECT DATE_ENTERED  FROM `blackboard_commands`
	WHERE CLIENT_ID != :CLIENT_ID 
	AND COMMAND = 1
	ORDER BY DATE_ENTERED 
	DESC LIMIT 1 ');
	$stmt->execute( array(':CLIENT_ID' => $clientid) );

	$rowcount = $stmt->rowCount();
	if ( $rowcount == 0)
	{
		$rowClear = false;
	}else
	{
		// if the last time the clear was issued (and not by us) and it was recent.. clear our screens too!
		$temp = $stmt->fetch();
		$timeEntered = strtotime($temp["DATE_ENTERED"]);

		//
		$time_diff =  time() - $timeEntered;
		if (($time_diff) < 25)
		{
			$rowClear = true;
		}
	}

	// now get all the new coords that are not ours.
	$stmt = $dbh->prepare('SELECT ID, X1,Y1,X2,Y2,WIDTH,COLOR FROM blackboard_points WHERE CLIENT_ID != :CLIENT_ID ');
	$stmt->execute(
	array(':CLIENT_ID' => $clientid)	);
	$result = $stmt->fetchAll();
}
catch (Exception $e)
{
	print "Error!: " . $e->getMessage() . "<br/>";
	die();
}
//set up our array to return.
$return  = array(0 => $rowClear,  1 => $result);
//http://php.net/manual/en/function.json-encode.php
header('Content-type: application/json');
echo json_encode($return);
?>



One last interesting php file is the 'get id'. If you notice here - I am limiting how many clients we can have connected at one time. Nothing overly fancy a if statement can't handle, but it does have some mildly complex logic involved.

<?php
// get relevent data
$clientid = $_POST["clientid"];
$version = $_POST["version"];
// sql connection statement
$dbh = new PDO('mysql:host=localhost;dbname=test', '123', '');
try
{
	//see how many ids we have logged in already
	$stmt = $dbh->prepare(' SELECT count(*) as idCount FROM `blackboard_client` ');
	$stmt->execute();
	$temp = $stmt -> fetch();
	$idCount = $temp["idCount"];

	// if more than 4 do not allow anyone else.. this can be modified in a admin panel for better control.
	if ($idCount < 4)
	{
		$stmt = $dbh->prepare('INSERT INTO `blackboard_client` (VERSION)
						VALUES(:VERSION) ');
		$stmt->execute(array(':VERSION' => $version	)
		);
		$clientid = $dbh->lastInsertId();
	}else
	{
		// if too many people logged in then do not give an id and do not let draw.
		$clientid = -1;
	}
}
catch (Exception $e)
{
	print "Error!: " . $e->getMessage() . "<br/>";
	die();
}
// reply with the client id
//http://php.net/manual/en/function.json-encode.php
header('Content-type: application/json');
echo json_encode($clientid);

?>



That should wrap up all the points of interest. It should be noted how, with a little planning, you can make a simple project (like the first tutorial), and expand it to easily bolt in new functionality.

Advance topics:
  • update the table with new colors after they have been in the table for X days
  • make an admin panel to store things like how many people can be on the page and the ability to kick people off
  • add a button to save the current screen to the server as well.. this may be writing the bytes to a table or just saving them to a directory
  • add the ability to send text on the screen to other active clients

Code

blackboard.html
Spoiler


styles.css
Spoiler


drawing.js
Spoiler


get_id.php
Spoiler


get_coords.php
Spoiler


set_coords.php
Spoiler


remove_id.php
Spoiler


clear.php
Spoiler


Is This A Good Question/Topic? 3
  • +

Page 1 of 1