Reputation: 866
I am creating a hexagonal grid using canvas, and I'm trying to fill each tile with a specific pattern from an image. The following code is what I'm working with.
The end result is a hexagonal grid that has tiles patterned all with the same image... and it shouldn't be. I think what is happening is that it's creating an overlay for the pattern for every tile, but that image basically covers all tiles... and I only ever end up seeing that last image called.
I was under the impression that my fill() was only filling that small hexagon shape... and not all of them. How can I make this so each individual hex shape can have it's own image?
This code is run in a for loop to create the grid. This is my drawHex() method. I can't imagine that I need a whole new canvas for each tile to make this happen.
var numberOfSides = 6,
size = hex.properties.radius,
Xcenter = hexObj.x + (hex.properties.width / 2),
Ycenter = hexObj.y + (hex.properties.height / 2);
var img = new Image();
if (hexObj.t == "grassland"){
img.src = "/static/grass.jpg";
}else{
img.src = "/static/mountain.jpg";
}
var pattern = context.createPattern(img, "repeat");
context.fillStyle = pattern;
context.beginPath();
context.moveTo (Xcenter + size * Math.cos(0), Ycenter + size * Math.sin(0));
for (var i = 1; i <= numberOfSides;i += 1) {
context.lineTo (Xcenter + size * Math.cos(i * 2 * Math.PI / numberOfSides), Ycenter + size * Math.sin(i * 2 * Math.PI / numberOfSides));
}
context.fill();
context.closePath();
context.stroke();
Upvotes: 1
Views: 1368
Reputation:
First important thing is to understand how image loading is working. This is an asynchronous operation which means that you have to wait for the image to load before continue.
This sort of will dictate the strategy as setting image source each time you want to change an image will also force you to handle the asynchronous aspect of it will the delays and so forth. Also, if the image(s) hasn't been loading at the time of creating a pattern the style will fail to render.
So a better approach is to preload all the images (or a single sprite-sheet) that you will use. Then hold on to them in memory (this is usually not a problem today if the images aren't gigantic).
You can then store several CanvasPattern
objects referencing each image. One way is to build a tile object holding all information about that tile incl. its pattern.
For example:
function Tile(ctx, x, y, radius, img) {
this.pattern = ctx.createPattern(img, "repeat");
// store other properties here like x, y, radius etc.
}
Tile.prototype.render = function(ctx) {
ctx.beginPath();
// create hex shape here
ctx.fillStyle = this.pattern;
ctx.fill();
}
Now you can create a Tile
object and hold it in an array (or a mother object):
var tiles = [];
tiles.push(new Tile(ctx, x1, y1, radius, img1); // img1 has loaded (onload)
tiles.push(new Tile(ctx, x2, y2, radius, img2); // img2 has also loaded (onload)
// etc.
Then simply render them at once like:
tiles.forEach(function(tile) { tile.render(ctx) });
You will need an image loader to load all images. The drawback is that the user has to wait for the images to load unless you have a front that can occupy the user meanwhile the images are loading in the background.
A loader doesn't have to be complicated, but for production you would want to handle errors (onerror
/onabort
). This example will call the function start()
when all images has loaded:
var images = [];
var urls = ["//image1.jpg", "//image2.jpg", ...];
var count = urls.length;
function handler() {
if (!--count) start();
}
urls.forEach(function(url) {
var img = new Image;
images.push(img);
img.onload = handler;
//img.onerror = ... // handler for error and abort here
//img.onabort = ...
img.src = url;
})
var images = [], urls = ["//i.imgur.com/DAg71N5.jpg?1", "//i.imgur.com/ZO3XQpj.jpg?1"],
tiles = [], count = urls.length, ctx = c.getContext("2d");
function handler() {if (!--count) start()}
function Tile(ctx, x, y, radius, img) {
this.pattern = ctx.createPattern(img, "repeat");
this.x = x;
this.y = y;
this.radius = radius;
}
Tile.prototype.render = function(ctx) {
ctx.beginPath();
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(this.x + Math.cos(i) * this.radius, this.y + Math.sin(i) * this.radius);
ctx.fillStyle = this.pattern;
ctx.fill();
}
urls.forEach(function(url) {
var img = new Image;
images.push(img);
img.onload = handler;
img.src = url;
});
function start() {
tiles.push(new Tile(ctx, 50, 50, 50, images[0]));
tiles.push(new Tile(ctx, 130, 95, 50, images[1]));
tiles.forEach(function(tile) { tile.render(ctx) });
}
<canvas id=c></canvas>
Hex shape can be drawn this way:
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(this.x + Math.cos(i) * this.radius, this.y + Math.sin(i) * this.radius);
Note that this requires beginPath()
. This allows us to pass on the moveTo()
as the first lineTo()
on the new path will move the path-cursor to its start point.
To keep patterns relative to the shape you can use translate()
on the context before drawing them relative to (0,0) which also simplify the hex drawing:
ctx.translate(this.x, this.y);
ctx.moveTo(this.radius, 0);
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(Math.cos(i) * this.radius, Math.sin(i) * this.radius);
// cancel transforms here if needed
In newer browsers you can use setTransform()
on the pattern itself. This is not supported in all browsers so be careful..
Mini-update For reuse purposes you can consider creating a pattern outside the object as part of the loading process, so that you only use a reference to the pattern for each tile.
The specs are a little unclear on what patterns do to images copy-wise. The only requirement is that changes to the image source must not affect the pattern after it has been created, which may or may not mean the image is copied internally, always or just when the condition for it demands it:
Modifying the image used when creating a CanvasPattern object after calling the createPattern() method must not affect the pattern(s) rendered by the CanvasPattern object.
In any case, there should be enough meat in the examples above to give you a basis and understanding to how to attack the problem. Modify as needed!
Upvotes: 6