putImageData
allows you to update the targeted pixels with the data from your ImageData, including transparent pixels.
There are various approaches to achieve this:
The simplest method involves manipulating pixels based on the current context state. Instead of using createImageData
, use getImageData
to obtain an ImageData containing the pixels in the selected area.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = 'black';
ctx.fillRect(20, 20, 40, 40);
// get the current pixels at (x, y, width, height)
var imgData = ctx.getImageData(10, 10, 100, 100);
var i;
// perform pixel manipulation on these pixels
for (i = 0; i < imgData.data.length; i += 16) {
imgData.data[i + 0] = 255;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
imgData.data[i + 3] = 255;
}
ctx.putImageData(imgData, 10, 10);
<canvas id="myCanvas"></canvas>
Note that while getImageData
consumes more memory and is slower than createImageData
, it may not be suitable for frequent usage, such as in animations.
In animation scenarios, consider utilizing a secondary off-screen canvas to draw your ImageData and then transfer it to the main canvas using drawImage
.
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = 'black';
var gridImage = generateGrid(100, 100);
var x = 0;
anim();
/*
Manipulate pixels on an off-screen canvas
Return the off-screen canvas
*/
function generateGrid(width, height){
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var off_ctx = canvas.getContext('2d');
var imgData = off_ctx.createImageData(width, height);
for (i = 0; i < imgData.data.length; i += 16) {
imgData.data[i + 0] = 255;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
imgData.data[i + 3] = 255;
}
off_ctx.putImageData(imgData, 0,0);
return canvas;
}
function anim(){
ctx.clearRect(0,0,c.width, c.height);
x = (x + 1) % (c.width + 100);
ctx.fillRect(x, 20, 40, 40);
// draw the generated canvas here
ctx.drawImage(gridImage, x - 10, 0);
requestAnimationFrame(anim);
}
<canvas id="myCanvas"></canvas>
Another option is to rearrange your code so that you execute the normal drawings (fillRect
) after applying the ImageData manipulations but beneath the current drawings. This can be achieved by adjusting the globalCompositeOperation
property of the context:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
// manipulate pixels first
var imgData = ctx.createImageData(100, 100);
var i;
for (i = 0; i < imgData.data.length; i += 16) {
imgData.data[i + 0] = 255;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
imgData.data[i + 3] = 255;
}
ctx.putImageData(imgData, 10, 10);
// set gCO to draw behind non-transparent pixels
ctx.globalCompositeOperation = 'destination-over';
// proceed with normal drawings
ctx.fillStyle = 'black';
ctx.fillRect(20, 20, 40, 40);
// reset gCO
ctx.globalCompositeOperation = 'source-over';
<canvas id="myCanvas"></canvas>
For modern browsers, consider leveraging the ImageBitmap object if applicable, as it produces the same results as the off-screen canvas with fewer lines of code:
(async () => {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.fillStyle = 'black';
var gridImage = await generateGrid(100, 100);
var x = 0;
anim();
/*
Modifies pixels within an empty ImageData
Returns a Promise resolving to an ImageBitmap
*/
function generateGrid(width, height) {
var imgData = ctx.createImageData(width, height);
for (i = 0; i < imgData.data.length; i += 16) {
imgData.data[i + 0] = 255;
imgData.data[i + 1] = 0;
imgData.data[i + 2] = 0;
imgData.data[i + 3] = 255;
}
return createImageBitmap(imgData);
}
function anim() {
ctx.clearRect(0, 0, c.width, c.height);
x = (x + 1) % (c.width + 100);
ctx.fillRect(x, 20, 40, 40);
// draw the generated canvas here
ctx.drawImage(gridImage, x - 10, 0);
requestAnimationFrame(anim);
}
})();
<canvas id="myCanvas"></canvas>