I want to draw a grid of 10 x 10 squares on a HTML5 canvas with number 1-100 displayed on the squares. Clicking a square should call a JavaScript function with the square's number passed as a variable to the function.
-
1Why do you need to do this in canvas? A hundred clickable squares with numbers in is trivial with HTML. – robertc Feb 04 '12 at 10:25
-
I'm new to this and don't know if I need to do it in canvas. How do I do it with HTML? I need to be able to change the background color of a single square though. – Fred Feb 04 '12 at 10:37
3 Answers
First, I encourage you to read this answer to another question involving the HTML5 Canvas. You need to understand that there are no squares. In order to detect a click on a 'square', you would have to keep track of a mapping from each canvas coordinate to the square(s) that it logically contains, handle a single click event on the entire canvas, work out which square(s) you want to change, and then redraw the canvas with the changes you want.
Then—since you seem to have no objection to using a more appropriate technology—I encourage you to do this in either HTML (where each 'square' is something like a <div>
that is absolutely-positioned and sized and colored using CSS), or SVG (using <rect>
if you need the squares to be able to be rotated, or want to introduce other shapes).
HTML and SVG are both 'retained-mode' graphics mode systems, where drawing a shape 'retains' the concept of that shape. You can move the shape, change its colors, size, etc. and the computer will automatically redraw it for you. Moreover, and more importantly for your use case, you can (with both HTML and SVG):
function changeColor(evt){
var clickedOn = evt.target;
// for HTML
clickedOn.style.backgroundColor = '#f00';
// for SVG
clickedOn.setAttribute('fill','red');
}
mySquare.addEventListener('click',changeColor,false);
Edit: I've created a simple implementation in JavaScript and HTML: http://jsfiddle.net/6qkdP/2/
Here's the core code, in case JSFiddle is down:
function clickableGrid( rows, cols, callback ){
var i=0;
var grid = document.createElement('table');
grid.className = 'grid';
for (var r=0;r<rows;++r){
var tr = grid.appendChild(document.createElement('tr'));
for (var c=0;c<cols;++c){
var cell = tr.appendChild(document.createElement('td'));
cell.innerHTML = ++i;
cell.addEventListener('click',(function(el,r,c,i){
return function(){ callback(el,r,c,i); }
})(cell,r,c,i),false);
}
}
return grid;
}
-
1
-
Thank you very much, I was trying to use canvas but this is a much better solution. Works perfect! – Fred Feb 05 '12 at 16:22
-
I know this is a very old post, but is it possible to make this table fill the entire parent container? I have made a customized version of this one, and I want it to fill a bootstrap grid column of `col-xs-6`, but it only fills the entire parent if the layout is small, like on a mobile device. – Zeliax May 04 '16 at 17:09
-
@Zeliax Please post this as a separate question. Your question is simply: _"Can I get an HTML table to fill a parent container, with fixed-aspect-ratio cell sizes?"_ It will be, I think, hard without JavaScript. Alternatively, create this in SVG and then you can scale the SVG to fill the container without issue. – Phrogz May 05 '16 at 17:12
-
Of course. I should do that. I did however in the meantime figure out how to solve my problem. Sorry for the inconvenience. – Zeliax May 05 '16 at 17:47
EDIT: Using HTML elements rather than drawing these things on a canvas or using SVG is another option and quite possibly preferable.
Following up on Phrogz's suggestions, see here for an SVG implementation:
jsfiddle example
document.createSvg = function(tagName) {
var svgNS = "http://www.w3.org/2000/svg";
return this.createElementNS(svgNS, tagName);
};
var numberPerSide = 20;
var size = 10;
var pixelsPerSide = 400;
var grid = function(numberPerSide, size, pixelsPerSide, colors) {
var svg = document.createSvg("svg");
svg.setAttribute("width", pixelsPerSide);
svg.setAttribute("height", pixelsPerSide);
svg.setAttribute("viewBox", [0, 0, numberPerSide * size, numberPerSide * size].join(" "));
for(var i = 0; i < numberPerSide; i++) {
for(var j = 0; j < numberPerSide; j++) {
var color1 = colors[(i+j) % colors.length];
var color2 = colors[(i+j+1) % colors.length];
var g = document.createSvg("g");
g.setAttribute("transform", ["translate(", i*size, ",", j*size, ")"].join(""));
var number = numberPerSide * i + j;
var box = document.createSvg("rect");
box.setAttribute("width", size);
box.setAttribute("height", size);
box.setAttribute("fill", color1);
box.setAttribute("id", "b" + number);
g.appendChild(box);
var text = document.createSvg("text");
text.appendChild(document.createTextNode(i * numberPerSide + j));
text.setAttribute("fill", color2);
text.setAttribute("font-size", 6);
text.setAttribute("x", 0);
text.setAttribute("y", size/2);
text.setAttribute("id", "t" + number);
g.appendChild(text);
svg.appendChild(g);
}
}
svg.addEventListener(
"click",
function(e){
var id = e.target.id;
if(id)
alert(id.substring(1));
},
false);
return svg;
};
var container = document.getElementById("container");
container.appendChild(grid(5, 10, 200, ["red", "white"]));
container.appendChild(grid(3, 10, 200, ["white", "black", "yellow"]));
container.appendChild(grid(7, 10, 200, ["blue", "magenta", "cyan", "cornflowerblue"]));
container.appendChild(grid(2, 8, 200, ["turquoise", "gold"]));

- 6,352
- 26
- 43
-
2+1 Nice work. Two minor suggestions: 1) More descriptive variable names for `N` and `S` might help a new user to understand and modify the code, and 2) When using `addEventListener`, ensure that you always pass the third parameter explicitly (`false`, in this case) or else Firefox gets angry. – Phrogz Feb 04 '12 at 21:12
-
-
Thank you very much for you suggestion. The HTML Phrogz suggested above does just what I need for now but as I develop the application maybe further functionality is needed. I have no knowledge of SVG or what it can do compared to the HTML solution? I will however learn more about this to see if your SVG method is better suited for this. – Fred Feb 05 '12 at 16:30
As the accepted answer shows, doing this in HTML/CSS is easiest if this is all your design amounts to, but here's an example using canvas as an alternative for folks whose use case might make more sense in canvas (and to juxtapose against HTML/CSS).
The first step of the problem boils down to figuring out where in the canvas the user's mouse is, and that requires knowing the offset of the canvas element. This is the same as finding the mouse position in an element, so there's really nothing unique to canvas here in this respect. I'm using event.offsetX/Y
to do this.
Drawing a grid on canvas amounts to a nested loop for rows and columns. Use a tileSize
variable to control the step amount. Basic math lets you figure out which tile (coordinates and/or cell number) your mouse is in based on the width and height and row and column values. Use context.fill...
methods to write text and draw squares. I've kept everything 0-indexed for sanity, but you can normalize this as a final step before display (don't mix 1-indexing in your logic, though).
Finally, add event listeners to the canvas element to detect mouse actions which will trigger re-computations of the mouse position and selected tile and re-renders of the canvas. I attached most of the logic to mousemove because it's easier to visualize, but the same code applies to click events if you choose.
Keep in mind that the below approach is not particularly performance-conscious; I only re-render when the cursor moves between cells, but partial re-drawing or moving an overlay to indicate the highlighted element would be faster (if available). There are a lot of micro-optimizations I've ignored. Consider this a proof-of-concept.
const drawGrid = (canvas, ctx, tileSize, highlightNum) => {
for (let y = 0; y < canvas.width / tileSize; y++) {
for (let x = 0; x < canvas.height / tileSize; x++) {
const parity = (x + y) % 2;
const tileNum = x + canvas.width / tileSize * y;
const xx = x * tileSize;
const yy = y * tileSize;
if (tileNum === highlightNum) {
ctx.fillStyle = "#f0f";
}
else {
ctx.fillStyle = parity ? "#555" : "#ddd";
}
ctx.fillRect(xx, yy, tileSize, tileSize);
ctx.fillStyle = parity ? "#fff" : "#000";
ctx.fillText(tileNum, xx, yy);
}
}
};
const size = 10;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 200;
const ctx = canvas.getContext("2d");
ctx.font = "11px courier";
ctx.textBaseline = "top";
const tileSize = canvas.width / size;
const status = document.createElement("pre");
let lastTile = -1;
drawGrid(canvas, ctx, tileSize);
document.body.style.display = "flex";
document.body.style.alignItems = "flex-start";
document.body.appendChild(canvas);
document.body.appendChild(status);
canvas.addEventListener("mousemove", evt => {
event.target.style.cursor = "pointer";
const tileX = ~~(evt.offsetX / tileSize);
const tileY = ~~(evt.offsetY / tileSize);
const tileNum = tileX + canvas.width / tileSize * tileY;
if (tileNum !== lastTile) {
lastTile = tileNum;
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid(canvas, ctx, tileSize, tileNum);
}
status.innerText = ` mouse coords: {${evt.offsetX}, ${evt.offsetX}}
tile coords : {${tileX}, ${tileY}}
tile number : ${tileNum}`;
});
canvas.addEventListener("click", event => {
status.innerText += "\n [clicked]";
});
canvas.addEventListener("mouseout", event => {
drawGrid(canvas, ctx, tileSize);
status.innerText = "";
lastTile = -1;
});

- 44,755
- 7
- 76
- 106