Summary
I've created a function that blends tiles on a 2D hexagonal grid with their neighboring tiles based on an image that serves as a "blend mask". It runs, and isn't too intensive, but I feel it can be better / faster. Drawing pixel-by-pixel is very intensive even for the 108x122 (Relatively small) images I'm using. With so many iterations per loop (13,176 in this case) per tile, even a small optimization could make a significant improvement.
I'll outline my process below, and I welcome all feedback and suggestions for optimization. If there's a way to achieve this without pixel-by-pixel updates I'd be intrigued to hear about it. I've noticed that context.drawImage()
seems to draw much faster than pixel-by-pixel updates and I'm not sure how that's achieved. Maybe I could utilize a similar process?
The Blend Mask
This is the (actual size) blend mask I've devised to control the most basic blending technique. There are six color ranges, each representing which tile to pull blend a texture from, and at which opacity:
- 0<Red<127: Top Right
- 0<Green<127: Mid Right
- 0<Blue<127: Bot Right
- 128<Red<255: Bot Left
- 128<Green<255: Mid Left
- 128<Blue<255: Top Left
Opacity is determined by the modulo of 127 into the corresponding R, G or B value. For example, a pixel defined as RGB(200, 0, 100) would mix the bottom left neighbor's texture at 200%127 = 73 opacity and the bottom right neighbor's texture at 100%127 = 100 opacity. You'd find such a pixel close to the bottom of this particular blend map.
I would like to continue using my blend mask method regardless of optimization capabilities due to the fact that I can alter this blend mask at any time, not only modifying the current shape but also adding more natural / chaotic looking blends to enhance aesthetics.
Example Blend
Relevant Code
My blend function. This is where over 90% of the actual processing takes place (Specifically in the pixel loop for(var i = 0; i < mask.width*mask.height*4; i+=4)
), so this is where I need to make the optimizations. All other code is provided solely to help understand the function's process.
function blend(mask, row, col) {
blender.drawImage(mask, 0, 0, mask.width, mask.height);
var imageData = blender.getImageData(0, 0, mask.width, mask.height);
var data = imageData.data;
var tileType = [
/*Top Right:*/ (row%2 == 0) ? getTile(row-1, col+1) : getTile(row-1, col+0),
/*Mid Right:*/ getTile(row+0, col+1),
/*Bot Right:*/ (row%2 == 0) ? getTile(row+1, col+1) : getTile(row+1, col+0),
/*Bot Left: */ (row%2 == 0) ? getTile(row+1, col+0) : getTile(row+1, col-1),
/*Mid Left: */ getTile(row+0, col-1),
/*Top Left: */ (row%2 == 0) ? getTile(row-1, col+0) : getTile(row-1, col-1),
];
// This is where over 90% of the processing occurs
for(var i = 0; i < mask.width*mask.height*4; i+=4) {
var colors = new Array();
var alphaSum = 0;
for(var side = 0; side < tileType.length; side++) {
var semi = 127 * Math.floor(side/3);
var index = i+side%3;
if(data[index] > semi && data[index] <= semi+128) {
if(tileType[side] != null) {
alphaSum += data[index] - semi;
colors.push({
r: images[tileType[side]].data[i+0],
g: images[tileType[side]].data[i+1],
b: images[tileType[side]].data[i+2],
a: data[index] - semi
});
}
}
}
if(colors.length > 1) {
var colorSum = { r:0, g:0, b:0 };
for(var index = 0; index < colors.length; index++) {
var alphaRatio = colors[index].a / alphaSum;
colorSum.r += colors[index].r * alphaRatio;
colorSum.g += colors[index].g * alphaRatio;
colorSum.b += colors[index].b * alphaRatio;
}
data[i+0] = colorSum.r;
data[i+1] = colorSum.g;
data[i+2] = colorSum.b;
data[i+3] = 255 - 255/((alphaSum+127)/127);
} else if (colors.length > 0) {
data[i+0] = colors[0].r;
data[i+1] = colors[0].g;
data[i+2] = colors[0].b;
data[i+3] = 255 - 255/((colors[0].a+127)/127);
} else {
data[i+3] = 0;
}
}
blender.putImageData(imageData, 0, 0);
}
My draw function, which calls my blend function once per tile:
function draw() {
var start = Date.now();
for(var row = 0; row < tiles.length; row++) {
for(var col = 0; col < tiles[row].length; col++) {
var tileType = tiles[row][col];
var indent = (row%2) ? 0.0 : 0.5 ;
var y = (images[tileType].height+4) * row * 0.75;
var x = images[tileType].width * (col+indent);
buffer.drawImage(images[tileType], 0, 0);
blend(blendMask, row, col);
ctx.drawImage(bCanvas, x, y, images[tileType].width, images[tileType].height);
ctx.drawImage(blCanvas, x, y, images[tileType].width, images[tileType].height);
}
}
console.log("Draw executed in " + (Date.now() - start) + "ms");
}
Additional Code
Declaring canvases and contexts:
canvas = $("#blendCanvas")[0];
ctx = canvas.getContext("2d");
bCanvas = $("#buffer")[0];
buffer = bCanvas.getContext("2d");
blCanvas = $("#blender")[0];
blender = blCanvas.getContext("2d");
My tile images array (Pseudo-code):
var images = new Array();
var imageURLs = [
"images/gameboard/grasshex.png",
"images/gameboard/sandhex.png",
"images/gameboard/dirthex.png"
];
for(var i = 0; i < imageURLs.length; i++) {
images.push(new Image());
images[i].src = imageURLs[i];
$(images[i]).load(function () {
// Draw image to a buffer canvas
this.data = buffer.getImageData().data;
}
Defining example tile grid (As seen above):
var tiles = [
[0, 1],
[2, 1, 2],
[1, 0]
];
Working Example
And of course, a working example: https://dl.dropbox.com/u/7377788/Other/Blend2/avblend.html
Thanks!
I hope this isn't too long-winded, and I'm looking forward to seeing if anybody has any suggestions for optimization. Thanks for reading!