Canvas 7, Saving and Loading

The last version of my Canvas app let you pick different brushes and draw on the grid. If you played around with the line brush you might have noticed a small bug. The line erases everything it passes over.  This is because the line has no idea what the image was supposed to look like before we started drawing.

Welcome to saving. If we save what the image looks like before we start drawing the line, then we won’t get this bug. (Plus we’ll have to save at some point anyway.)

So how do we save? We could get the color of ever cell and save it into an array. Or we could save the entire canvas as an actual image. I’m not 100% on which method is faster yet, but I’m betting it’s the image method. (I base this assumption on poking around the Canvas forums on the internet.)

To save the canvas, we are going to use the .toDataURL() method. This returns a data url string of the current canvas image.

To load the image, we need to simply draw what we saved right? Turn the data url into an image object by setting it as the source, and then use .drawImage() to draw it. Except this doesn’t work. If you set an img tag on the page to have a src of the data url, then it does work! I’m not 100% of why this is the case, but I can solve it. You need to draw the image on the canvas in the image’s onload event. Then you set the source and everything works as expected.

Let me show you.

//
// Get the data URL of the Canvas
//
function getImage()
{
	return $("#canvas").toDataURL();
}

//
// Load the Data URL image
//
function setImage(dataURL)
{
	var img = new Image();
	img.onload = function(){
		//Have to draw on the Canvas here for the image to load properly
		$("#canvas")[0].getContext("2d").drawImage(img, 0, 0);
	}
	img.src = dataURL;

}

So my first thought to solve the line problem was to save the image before we start drawing the line. Then each update restore the original image and draw our new line on top of it. The problem with this is that img.onload issue. I have to draw the line inside of that method or else it won’t show up. So I modified the setImage method to take a function as a paramter. Now I could update my Brush’s onMouseMove like so

//
// Load the Data URL image
//
setImage: function(dataURL, drawOperations)
{
	var img = new Image();
	img.onload = function(){
		//Have to draw on the Canvas here for the image to load properly
		Grid.display.drawImage(img, 0, 0);
		//Do any other draw operations in here
		if( undefined != drawOperations ){
			drawOperations();
		}
	}
	img.src = dataURL;
},

//
// Draw the line
//
onMouseMove: function(evt)
{
	if( LineBrush.shouldDraw )
	{
		Grid.setImage( LineBrush.orginalData, function(){
			//Get the Cell from the grid
			LineBrush.overCell = Grid.getCellFromEvent(evt);

			//Draw the new line
			LineBrush.drawLine( LineBrush.color1 );
                });
	}
},

Here is the final result.

Canvas Test 7


And the code

		//
		// Orginal From http://www.javascripter.net/faq/rgbtohex.htm
		// Added PDAtoHex()
		//
		function PDAtoHex(pixalDataArray)
		{
			return "#" + toHex(pixalDataArray[0]) + toHex(pixalDataArray[1]) + toHex(pixalDataArray[2]);
		}
		function toHex(N) {
		 if (N==null) return "00";
		 N=parseInt(N); if (N==0 || isNaN(N)) return "00";
		 N=Math.max(0,N); N=Math.min(N,255); N=Math.round(N);
		 return "0123456789ABCDEF".charAt((N-N%16)/16)
		      + "0123456789ABCDEF".charAt(N%16);
		}

		//
		// Controls the Grid
		var Grid = {
			canvasID: "display",	//ID for the canvas element
			display: null,			//The Drawing context
			jQueryCanvas: null,		//jQuery object for the canvas
			//Canvas Size
			canvasWidth: 500,
			canvasHeight: 400,
			//Columns and Rows
			numberOfColumns: 25,
			numberOfRows: 25,
			//Colors for the Grid
			backgroundColor: "#FFF",
			gridColor: "#ccf",
			gridThickness: "2",
			//Size of the cells
			cellWidth:  function() { return this.canvasWidth/this.numberOfColumns; },
			cellHeight: function() { return this.canvasHeight/this.numberOfRows; },

			//
			//Draw the Grid
			// Uses the gridColor and gridThickness
			drawGrid : function(){
				//Set the colors
				this.display.strokeStyle = this.gridColor;
				this.display.lineWidth = this.gridThickness;

				//Start Draw
				this.display.beginPath();

				//Draw the Vertical lines
				for(var i=0; i <= this.numberOfColumns; i++){
					//Move to the top of the canvas
					this.display.moveTo(i*this.cellWidth(), 0)
					//Draw the line
					this.display.lineTo( i*this.cellWidth(), this.canvasHeight );
				}

				//Draw the Horizontal lines.
				for(var i=0; i <= this.numberOfRows; i++){
					//Move to the left of the canvas
					this.display.moveTo(0, i*this.cellHeight())
					//Draw the line
					this.display.lineTo( this.canvasWidth, i*this.cellHeight() );
				}
				//Stop Draw
				this.display.stroke();
			},
			//
			//Draw the Background
			// Uses backgroundColor
			drawBackground : function(){
				this.display.fillStyle = this.backgroundColor;
				this.display.fillRect(0,0, this.canvasWidth, this.canvasHeight);
			},
			//
			//Draw/color a cell
			// Takes: Cell object with column,row
			drawCell : function(cell, color){
				//Figure out the position
				var y_pos = cell.row * this.cellHeight();
				var x_pos = cell.column * this.cellWidth();

				//Set the Color
				this.display.fillStyle = color;
				//Draw the cell
				this.display.fillRect( x_pos, y_pos, this.cellWidth(), this.cellHeight());
				//Draw the cell outline
				this.display.strokeRect( x_pos, y_pos, this.cellWidth(), this.cellHeight());
			},

			//
			//Get the Cell The Mouse is in
			// Returns: Cell object with column,row
			getCellFromEvent : function(evt){
				var offset = this.jQueryCanvas.offset();
				//Create an Object to hold the column/row
				var cell = {
					column: -1,
					row: -1,
				};

				//Find the poistion of the mouse
				var x_pos = Math.floor(evt.pageX - offset.left);
				var y_pos = Math.floor(evt.pageY - offset.top);

				//Now which column was it clicked?
				for(var column=0; column < this.numberOfColumns; column++){
					//Find the column
					if( x_pos >= column*this.cellWidth() && x_pos <= (column+1)*this.cellWidth() ){
						//Found it!
						cell.column = column;
						//Fow find the row
						for(var row=0; row < this.numberOfRows; row++){
							//Find the row
							if( y_pos >= row*this.cellHeight() && y_pos <= (row+1)*this.cellHeight()){
								//Found it!
								cell.row = row;
							}
						} //End Row
					}
				} // End Column

				//Return the found position
				return cell;
			},
			//
			//Gets the color of a cell
			// Returns: CanvasPixelArray
			getColorFromCell: function(cell){
				//Figure out the position
				var y_pos = cell.row * this.cellHeight();
				var x_pos = cell.column * this.cellWidth();

				return this.display.getImageData( x_pos+1, y_pos+1, 1, 1).data;
			},

			//
			//Initalize the Grid
			//
			init: function(){
				this.jQueryCanvas = $("#"+this.canvasID);

				this.jQueryCanvas.attr({
					width: this.canvasWidth,
					height: this.canvasHeight
				});

				//Set the Drawing context
				this.display = this.jQueryCanvas[0].getContext("2d");

				//Draw the Background
				this.drawBackground();
				//Draw the Grid
				this.drawGrid();
			},

			//
			// Set the Brush to Use
			//
			setBrush: function(brushObject)
			{
				this.jQueryCanvas
					//UnBind all the Old brush stuff
					.unbind("mousedown")
					.unbind("mousemove")
					.unbind("mouseup")
					//Bind our Grid to the Brushes Methods
					.bind("mousedown", brushObject.onMouseDown)
					.bind("mousemove", brushObject.onMouseMove)
					.bind("mouseup", brushObject.onMouseUp)
			},

			//
			// Get the data URL of the Canvas
			//
			getImage: function()
			{
				return this.jQueryCanvas[0].toDataURL();
			},

			//
			// Load the Data URL image
			//
			setImage: function(dataURL, drawOperations)
			{
				var img = new Image();
				img.onload = function(){
					//Have to draw on the Canvas here for the image to load properly
					Grid.display.drawImage(img, 0, 0);
					//Do any other draw operations in here
					if( undefined != drawOperations ){
						drawOperations();
					}
				}
				img.src = dataURL;
			},
		};

		//
		// Simple Brush
		//	Colors Primary on Click, Secondary if primary is already the color.
		var SimpleDrawBrush = {
			//Colors used for the brush
			color1: "#00FF00",
			color2: "#FFFFFF",

			onMouseDown: function(evt){
				//We Used jQuery to bind it, so we are getting a jQuery object as this
				//Get the Cell from the grid
				var cell = Grid.getCellFromEvent(evt);

				//Get the current color of the cell.
				var color = Grid.getColorFromCell(cell);

				//Is it already the Primary color?
				if( PDAtoHex(color) == SimpleDrawBrush.color1 ){
					//Yes, use the secondary color
					Grid.drawCell(cell, SimpleDrawBrush.color2);
				} else {
					//Use the Primary Color
					Grid.drawCell(cell, SimpleDrawBrush.color1);
				}
			},

			onMouseMove: function(evt)
			{
				//Do nothing, this method just has to be defined.
			},

			onMouseUp: function(evt)
			{
				//Do nothing, this method just has to be defined.
			},

		};

		//
		// Hold Brush
		//	Keeps coloring for as long as the mouse in down.
		var HoldDrawBrush = {
			//Color for the brush
			color1: "#FF0000",
			shouldDraw: false,

			//
			// Start the coloring process
			onMouseDown: function(evt)
			{
				//Get the Cell from the grid
				var cell = Grid.getCellFromEvent(evt);
				//Use the Primary Color
				Grid.drawCell(cell, HoldDrawBrush.color1);
				//Tell it we should be drawing
				HoldDrawBrush.shouldDraw = true;
			},

			//
			// Continue Coloring
			onMouseMove: function(evt)
			{
				if( HoldDrawBrush.shouldDraw )
				{
					//Get the Cell from the grid
					var cell = Grid.getCellFromEvent(evt);
					//Use the Primary Color
					Grid.drawCell(cell, HoldDrawBrush.color1);
				}
			},

			onMouseUp: function(evt)
			{
				//Stop drawing
				HoldDrawBrush.shouldDraw = false;
			},

		};

		//
		// Line Brush
		//
		var LineBrush = {
			color1: "#0000FF",
			color2: "#FFFFFF",
			shouldDraw: false,
			startCell: {column: -1, row: -1},
			overCell: {column: -1, row: -1},
			orginalData: null,

			onMouseDown: function(evt)
			{
				//First, save the image as it currently stands
				LineBrush.orginalData = Grid.getImage();
				//Get the Cell from the grid
				LineBrush.startCell = Grid.getCellFromEvent(evt);
				//Use the Primary Color
				Grid.drawCell(LineBrush.startCell, LineBrush.color);
				//Tell it we should be drawing
				LineBrush.shouldDraw = true;
			},

			onMouseMove: function(evt)
			{
				if( LineBrush.shouldDraw )
				{
					Grid.setImage( LineBrush.orginalData, function(){
						//Get the Cell from the grid
						LineBrush.overCell = Grid.getCellFromEvent(evt);

						//Draw the new line
						LineBrush.drawLine( LineBrush.color1 );
					});
				}
			},

			onMouseUp: function(evt)
			{
				//Stop drawing
				LineBrush.shouldDraw = false;
			},

			//
			// Draws a line from the start cell to the over cell.
			//
			drawLine: function(color)
			{
				//Save as varibles for ease of reading
				var a_column = LineBrush.startCell.column;
				var a_row = LineBrush.startCell.row;
				var b_column = LineBrush.overCell.column;
				var b_row = LineBrush.overCell.row;

				//Get the length of the line
				var line_length = Math.sqrt( Math.pow( a_column - b_column, 2) + Math.pow( a_row - b_row, 2));
				//Now draw each point
				for(var i=0; i < line_length; i++){
					//Get the Row/Column of the cell to color
					var cell = {};
					cell.column = Math.round( b_column + ( a_column - b_column)*i/line_length);
					cell.row = Math.round( b_row + ( a_row - b_row)*i/line_length);

					//Now redraw it
					Grid.drawCell( cell, color);
				}
			},
		};
Share

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

CommentLuv Enabled