Still learning Canvas. How can I fade out each tile in an image that I have as Image Data in an array:
for (let i = 0; i < tilesY; i++) {
for (let j = 0; j < tilesX; j++) {
this.ctxTile.putImageData(this.tileData[itr], j * tileWidth, i * tileHeight);
itr += 1
}
}
I understand that the solution must have something to do with compositing? I want to fade out each tile individually. The putImageData works and the image is inside canvas, and assembled as a set of tiles.
Thanks
Usually you'd just play with the ctx.globalAlpha
property at the time of drawing to your context to set your tile's alpha. However, putImageData
is kind of a strange beast in the API in that it ignores the context transformation, clipping areas and in our case compositing rules, including globalAlpha
.
So one hack around would be to "erase" the given tile after we've drawn it. For this we can use the globalCompositeOperation = "destination-out"
property that we'll use on a call to a simple fillRect()
with the inverse globalAlpha
we want. (Luckily putImageData
always draws only rectangles).
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.img = buildImageData(width, height);
this.alpha = 1;
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
ctx.putImageData(this.img, this.x, this.y);
ctx.globalAlpha = 1 - this.alpha; // inverse alpha
// the next drawing will basically erase what it represents
ctx.globalCompositeOperation = "destination-out";
ctx.fillRect(this.x, this.y, this.width, this.height);
// restore the context
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = "source-over";
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
redraw();
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>
However this means that for each tile we have one putImageData
+ one composited fillRect
. If you've got a lot of tiles, that makes for a pretty big overhead.
So instead the best might be to convert all your ImageData objects to ImageBitmap ones. To understand the difference between both I invite you to read this answer of mine.
Once we have ImageBitmaps, we can apply the globalAlpha
on our draw call directly:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const tileWidth = 50;
const tileHeight = 50;
class Tile {
constructor(x, y, width, height) {
this.x = Math.round(x); // putImageData only renders at integer coords
this.y = Math.round(y);
this.width = width;
this.height = height;
this.alpha = 1;
const imgData = buildImageData(width, height);
// createImageBitmap is "async"
this.ready = createImageBitmap(imgData)
.then((bmp) => this.img = bmp);
}
isPointInPath(x, y) {
return x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
draw() {
// single draw per tile
ctx.globalAlpha = this.alpha;
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
ctx.globalAlpha = 1;
}
}
const tiles = Array.from({ length: 5 }, (_, i) => new Tile(i * tileWidth * 1.25, 0, tileWidth, tileHeight));
canvas.onclick = (e) => {
const x = e.clientX - canvas.offsetLeft;
const y = e.clientY - canvas.offsetTop;
const clickedTile = tiles.find((tile) => tile.isPointInPath(x, y));
if (clickedTile) { clickedTile.alpha -= 0.1 };
redraw();
};
// wait for all the ImageBitmaps are generated
Promise.all(tiles.map((tile) => tile.ready)).then(redraw);
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
tiles.forEach((tile) => tile.draw());
}
function buildImageData(width, height) {
const img = new ImageData(width, height);
const arr = new Uint32Array(img.data.buffer);
for (let i = 0; i < arr.length; i++) {
arr[i] = Math.random() * 0xFFFFFF + 0xFF000000;
}
return img;
}
<p>Click on each tile to lower its alpha.</p>
<canvas></canvas>