Drawing Table

Chris Coyier //

I made a thing, in which you can use your mouse cursor to draw on a grid with different colors. You can then copy-and-paste the HTML from the design you made.


View Demo   Download Files

Features

  • Clearing the current design
  • Changing the grid size to different preset options
  • Color swatches to change the currently active color
  • Color picker to change the swatch colors
  • White color = erasing
  • Holding the [option] key will enter erase mode, lifting up that key goes back to the color you were on.
  • Ability place image inside the grid to trace from (dims the grid above it)
  • Toggle tracing mode on and off
  • And the point of all this... ability to copy and paste the HTML of the finished design.

What's the point?

I was at a presentation by @blueys where she was talking about HTML email and showed some particularly nice examples. One that I thought was extra cool was this one that she found from the Campaign Monitor gallery.

"Inky", the blue ghost from PacMan shown above, was created with no images, just using table cells with background colors applied. This is particularly cool for HTML emails because in many (most?) email clients these days, image ares not displayed until a user explicitly clicks a link to choose to display them. By using color table cells, you can display simple graphics without using actual images.

Building this Mini App

This is essentially a one-page JavaScript (jQuery) powered mini application. Let's cover how some of the different parts of it work.

Building the grid

The drawing table itself is quite literally an HTML table. We could put table markup in the HTML itself, but it's far more flexible to have JavaScript build the markup. That way we can change the tables structure programatically rather than through literally altering HTML. This also allows us to build new size tables on the fly. One of the features is a dropdown menu for different grid sizes, so let's actually put our grid building JavaScript in a function we can call when that changes.

function buildGrid(cols, rows) {

	var tableMarkup = "";

	for (x = 0; x < cols; x++) {
		tableMarkup += "<tr>";
		for (y = 0; y < rows; y++) {
			tableMarkup += "<td>&nbsp;</td>";
		}
		tableMarkup += "</tr>";	
	}

	$("#drawing-table").html(tableMarkup)

};

This creates an empty string, then has an outer loop which runs as many times as the passed rows parameter and an inner loops which runs as many times as the passed cols parameter. For each row, wrapping <tr> tags are appened to the string, and for each column, <td>'s are inserted.

When done, the grid's html is replaced with the new markup we just created. I was a bit curious about the speed of string concatenation for this kind of thing, but it seems like it's not too bad a way to go. The alternatives being creating and appending elements on the fly, and using arrays. See this Forrst thread for more info.

Now we can set some variables for the initial rows and columns, and call the function.

 var cols = 20, rows = 20;

// Inital Build of Table 
buildGrid(cols, rows);

The HTML for our grid size dropdown menu will be like this:

<select id="gridSize">
	<option value="10,10">10 x 10</option>
	<option value="20,20" selected>20 x 20</option>
	<option value="30,30">30 x 30</option>
</select>

And then we'll watch for that dropdown to change value, and re-call the buildGrid() function when it does.

// Dropdown for changing Grid Size
$("#gridSize").change(function() {
	$el = $(this);
	rows = $el.val().split(",")[0];
	cols = $el.val().split(",")[1];
	buildGrid(rows, cols);
});

Similarly, clearing the design just checks the current setting of the dropdown and rebuilds the grid with that size.

The actual drawing

We need to get a touch clever with the mouse and how we accomplish the click-and-drag drawing feature. We clearly can't just attach click events to the cells, as that would make drawing tedious. We'll need to use the mouseenter event, but also know if the mouse button is current down or not. Let's think it out.

  • On mousedown of any table cell, toggle the drawing state to on
  •   - If the erase state is on, remove styling from cell
  •   - If the erase state is off, apply coloring to cell
  • On mouseenter of any table cell, check if drawing state is on
  •   - if on, color cell
  • On mouseout anywhere, toggle the drawing state off
// Drawing functionality
$("#drawing-table").delegate("td", "mousedown", function() {
	mouseDownState = true;
	$el = $(this);
    if (eraseState) {
    	$el.removeAttr("style");
    } else {
    	$el.css("background", curColor);
    }
}).delegate("td", "mouseenter", function() {
	if (mouseDownState) {
		$el = $(this);
	    if (eraseState) {
	    	$el.removeAttr("style");
	    } else {
	    
	    	// DRAWING ACTION
	    	$el.css("background", curColor);
	    }
	}
});
$("html").bind("mouseup", function() {
	mouseDownState = false;
});

Erasing mode

Our drawing mode is all ready to deal with erasing as well as coloring, so all we need to do is ensure that the eraseState variable is properly set to true or false accordingly. The first way to enable it is to click the white circle. Note in the HTML below, the data-color attribute is used to hold the color value for the three color swatches, but for the fourth/white/eraser circle, the value is "eraser".

<fieldset id="color-selector">
	<legend>Color Picker</legend>
	<div class="color red selected" data-color="red"><input type="text"></div>
	<div class="color green" data-color="green"><input type="text"></div>
	<div class="color blue" data-color="blue"><input type="text"></div>
	<div class="color eraser" data-color="eraser"></div>
	<p>Hold [Option] key for temporary erase mode</p>
</fieldset>

When one of the circles is clicked, if it is a color, the current color will be set to that swatch color and erase mode turned off. If it is the eraser that was clicked, erase mode is toggled on. A selected class is also applied to give visual feedback of the change.

// Color selection swatches
$("#color-selector").delegate(".color", "click", function() {
	
	$el = $(this);
	var pulledVal = $el.attr("data-color");
	
	if (pulledVal == 'eraser') {
		eraseState = true;
	} else {
		eraseState = false;
		curColor = pulledVal;
	}
	
	$(".color").removeClass("selected");
	$(this).addClass("selected");
});

We also wrote in the markup that you can hold the [option] key to toggle erase mode. This makes drawing much easier, being able to switch between modes without having to move the mouse over and select the eraser manually. To do this, we'll watch for keydown and keyup events on the document. If the key happens to be 18 (the option key), we'll turn erase mode on and off accordingly, as well as apply that selected class for more visual feedback.

// Erasing functionality through OPTION key
$(document).keydown(function(event) {
	if (event.keyCode == 18) {
		eraseState = true;
		$(".selected").addClass("previous");
		$(".color").removeClass("selected");
		$(".eraser").addClass("selected");
		
	}
}).keyup(function(event) {
	if (event.keyCode == 18) {
		eraseState = false;
		$(".color").removeClass("selected");
		$(".previous").addClass("selected").removeClass("previous");
		$("." + curColor).addClass("selected");
	}
});

Color picker

I used this jQuery Color Picker. Notice in the HTML for the color swatches above each of them had an <input type="text" /> inside of the <div>. Those inputs are used for the colorpicker, to store the value.

To each of those text inputs, we'll bind the colorpicker. When a choice is made, we'll update the visual color of the swatch as well as its data-color attribute for when it's switched away from and back to.

$('.color input').ColorPicker({
	onSubmit: function(hsb, hex, rgb, el) {
	
		var $swatch = $(el).parent();
		var newColor = "#" + hex;
		
		$(".color").removeClass("selected");
		$("." + $swatch.attr("data-color")).css("background", newColor).addClass("selected");
		$swatch.attr("data-color", newColor);
		curColor = newColor;
		    		    		
	},
	onBeforeShow: function () {
		$(this).ColorPickerSetColor(this.value);
	}
});

Tracing mode

When the URL to an image is entered into the text input provided for that, and it submitted, we want to create a new <div> underneath the table and show that image. It will be sized exactly the same as the table, and set as a background image so if the image is larger it will just get cut off. We'll also have a tracing mode, where we dim the opacity of the table while the image is viewable. To help check the final progress, we'll have a button toggle for turning off tracing mode. Turning it back on is still just a click of a button though.

// Tracing Functionality

$("#tracing-image-form").submit(function() {
	
		var url = $("#fileLocation").val();
					
		$("<div />", {
		
			css: {
				backgroundImage: "url(" + url + ")",
				width: 500,
				height: 500,
				opacity: 1,
				position: "absolute",
				top: 0,
				left: 0
			},
			id: "tracing-image"
		
		}).appendTo("#table-wrap");
		
		$("#drawing-table").css("opacity", 0.5);
		$("#toggle-tracing-mode").show(); 
		$("#tracing-image-form").remove();
		tracingMode = true;	
					
		return false;

	});

$("#toggle-tracing-mode").click(function() {

	if (tracingMode) {
		$("#tracing-image").css("visibility", "hidden");
		$(this).html("Toggle Tracing Mode On");
		$("#drawing-table").css("opacity", 1);
		tracingMode = false;
	} else {
		$("#tracing-image").css("visibility", "visible");
		$(this).html("Toggle Tracing Mode Off");
		$("#drawing-table").css("opacity", 0.5);
		tracingMode = true;
	}

});

That can probably be handled more elegantly than that, but it does the trick.

Supplying the final HTML

The purpose of this whole mini app is to deliver the HTML of the designed table. Fortunately this is incredibly easy. We have a textarea and a button. When the button is clicked, the HTML is gathered from the table and put into the textarea.

$("#get-html-button").click(function() {
	$("#the-html").val("<table style='width: 100%; border-collapse: collapse;'>" + $("#drawing-table").html() + "</table>");
});

Wrap Up

I think I might do a screencast of all this and talk through it all, so watch for that. Not every single detail and line of code is present in the written stuff above, I just broke apart the modules that are the most interesting. To view the complete code, download the example and play. If you do anything fun with it, share!

View Demo   Download Files