我在使用 canvas putImageData 从另一个 canvas/image(png) 复制透明像素时遇到问题
I'm having troubles with copying transparent pixels with canvas putImageData from another canvas/image(png)
我正在尝试复制描述的方法 here on Whosebug。
但是我遇到了一些不知道如何解决的问题。
我设置jsfiddle来演示一切。
这是 second jsfiddle 只有粒子在移动和被绘制。
我的问题在于绘图,探查器显示大约 10000 个粒子 drawImage 占用了总循环时间的 40%。没有直接画图,只有计算,没有什么阻碍代码执行所以问题出在画图上。
有没有办法在没有这些副作用的情况下使用这种技术?目前,我向您展示了我如何使用圆弧创建圆形区域,但我也将 png 文件用于其他一些对象,它们表现出完全相同的行为。
(问题:黑色重叠区域不是透明区域,通过上面的圆可以看到底部圆的边缘)
我希望我表达得尽可能清楚(上图很清楚地显示了我的问题),非常感谢您的帮助。
绘制函数 - 最终绘制可见 canvas。
Game.prototype.draw2 = function(interpolation, canvas, ctx, group)
{
var canvasData = ctx.createImageData(canvas.width, canvas.height),
cData = canvasData.data;
for (var i = 0; i < group.length; i++)
{
var obj = group[i];
if(!obj.draw)
{
continue;
}
var imagePixelData = obj.imagePixelData;
var x = obj.previous.x + (obj.x - obj.previous.x) * interpolation;
var y = obj.previous.y + (obj.y - obj.previous.y) * interpolation;
for (var w = 0; w < obj.width; w++)
{
for (var h = 0; h < obj.height; h++)
{
if (x + w < canvas.width && obj.x + w > 0 &&
y + h > 0 && y + h < canvas.height)
{
var iData = (h * obj.width + w) * 4;
var pData = (~~ (x + w) + ~~ (y + h) * canvas.width) * 4;
cData[pData] = imagePixelData[iData];
cData[pData + 1] = imagePixelData[iData + 1];
cData[pData + 2] = imagePixelData[iData + 2];
if (cData[pData + 3] < 100)
{
cData[pData + 3] = imagePixelData[iData + 3];
}
}
}
}
}
ctx.putImageData(canvasData, 0, 0);
};
下面是我在其他不可见的地方准备粉红色圆形区域的方法canvas。
Game.prototype.constructors.Attractor.prototype.getImageData = function(context)
{
this.separateScene = new context.constructors.Graphics(this.width, this.height, false);
this.image = this.separateScene.canvas;
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.radius, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = '#ff9b9b';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.radiusCut, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = 'rgba(255, 255, 255, 0.27)';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.coreRadius, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = '#ff64b2';
this.separateScene.ctx.fill();
this.imageData = this.separateScene.ctx.getImageData(0, 0, this.width, this.height);
this.imagePixelData = this.imageData.data;
};
寻找黑色像素
@Loktar 的最佳答案是针对特定图像做出的,该图像仅由黑色和透明像素组成。
在imageData中,这两种类型的像素非常相似,只是它们的alpha值不同。所以他的代码只是对 alpha 值(每个循环中的第四个)进行 should-draw 检查。
cData[pData] = imagePixData[iData];
cData[pData + 1] = imagePixData[iData + 1];
cData[pData + 2] = imagePixData[iData + 2];
// only checking for the alpha value...
if(cData[pData + 3] < 100){
cData[pData + 3] = imagePixData[iData + 3];
}
另一方面,您正在处理彩色图像。因此,当这部分针对透明像素执行时,并且您在此位置已经有一个彩色像素,前三行会将现有像素转换为透明像素的 rgb 值(0,0,0
)但保留 alpha 值现有像素(在您的情况下 255
)。
然后您会得到一个黑色像素,而不是之前此处的彩色像素。
要解决它,您可以将整个块包裹在检查当前 imagePixData
的不透明度的条件中,而不是检查已经绘制的块。
if (imagePixelData[iData+3]>150){
cData[pData] = imagePixelData[iData];
cData[pData + 1] = imagePixelData[iData + 1];
cData[pData + 2] = imagePixelData[iData + 2];
cData[pData + 3] = imagePixelData[iData + 3];
}
对抗白人
那些白色像素在这里是因为 anti-aliasing。它已经存在于@Loktar 的原始示例中,只是由于他的图像大小而不太明显。
当您处理 imageData 时,这些伪影是垃圾,因为我们只能修改每个像素,而我们不能在子像素上设置值。换句话说,我们不能使用抗锯齿。
这就是原始检查中的 <100
或我上面的解决方案中的 >150
的目的。
您在此检查中针对 alpha 值采用的最小范围,您获得的伪影越少。但另一方面,你的边框会越粗糙。
你必须自己找到正确的值,但圆圈是最差的,因为几乎每个边框像素都会被消除锯齿。
改进真棒(a.k.a我可以给你10000张彩图)
你的实际实施让我想到了可以对@Loktar 的解决方案进行一些改进。
不是保留原始图像的数据,我们可以对每个像素进行第一次循环,并存储一个新的 imageData 数组,该数组由六个槽组成:[x, y, r, g, b ,a]
。
这样,我们可以避免存储所有不需要的透明像素,从而减少每次调用的迭代次数,而且我们还可以避免在每次循环中进行任何 alpha 检查。最后,我们甚至不需要 "get the position pixel from the image canvas" 因为我们为每个像素存储了它。
这是一个带注释的代码示例作为概念证明。
var parseImageData = function(ctx) {
var pixelArr = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
// the width of our image
var w = ctx.canvas.width;
// first store our image's dimension
var filtered = [];
// loop through all our image's pixels
for (var i = 0; i < pixelArr.length; i += 4) {
// we don't want traparent or almost transparent pixels
if (pixelArr[i + 3] < 250) {
continue;
}
// get the actual x y position of our pixel
var f = (i / 4) / w;
var y = Math.floor(f);
var x = Math.round((f - y) * w);
// add the pixel to our array, with its x y positions
filtered.push(x, y, pixelArr[i], pixelArr[i + 1], pixelArr[i + 2], pixelArr[i + 3]);
}
return filtered;
};
// here we will store all our pixel arrays
var images = [];
// here we will store our entities
var objects = [];
var draw = function() {
// create a new empty imageData of our main canvas
var imageData = mainCtx.createImageData(mainCanvas.width, mainCanvas.height);
// get the array we'll write onto
var pixels = imageData.data;
var width = mainCanvas.width;
var pixelArray,
deg = Math.PI / 180; // micro-optimizaion doesn't hurt
for (var n = 0; n < objects.length; n++) {
var entity = objects[n],
// HERE update your objects
// some fancy things by OP
velY = Math.cos(entity.angle * deg) * entity.speed,
velX = Math.sin(entity.angle * deg) * entity.speed;
entity.x += velX;
entity.y -= velY;
entity.angle++;
// END update
// retrieve the pixel array we created before
pixelArray = images[entity.image];
// loop through our pixel Array
for (var p = 0; p < pixelArray.length; p += 6) {
// retrieve the x and positions of our pixel, relative to its original image
var x = pixelArray[p];
var y = pixelArray[p + 1];
// get the position of our ( pixel + object ) relative to the canvas size
var pData = (~~(entity.x + x) + ~~(entity.y + y) * width) * 4
// draw our pixel
pixels[pData] = pixelArray[p + 2];
pixels[pData + 1] = pixelArray[p + 3];
pixels[pData + 2] = pixelArray[p + 4];
pixels[pData + 3] = pixelArray[p + 5];
}
}
// everything is here, put the image data
mainCtx.putImageData(imageData, 0, 0);
};
var mainCanvas = document.createElement('canvas');
var mainCtx = mainCanvas.getContext('2d');
mainCanvas.width = 800;
mainCanvas.height = 600;
document.body.appendChild(mainCanvas);
// just for the demo
var colors = ['lightblue', 'orange', 'lightgreen', 'pink'];
// the canvas that will be used to draw all our images and get their dataImage
var imageCtx = document.createElement('canvas').getContext('2d');
// draw a random image
var randomEgg = function() {
if (Math.random() < .8) {
var radius = Math.random() * 25 + 1;
var c = Math.floor(Math.random() * colors.length);
var c1 = (c + Math.ceil(Math.random() * (colors.length - 1))) % (colors.length);
imageCtx.canvas.width = imageCtx.canvas.height = radius * 2 + 3;
imageCtx.beginPath();
imageCtx.fillStyle = colors[c];
imageCtx.arc(radius, radius, radius, 0, Math.PI * 2);
imageCtx.fill();
imageCtx.beginPath();
imageCtx.fillStyle = colors[c1];
imageCtx.arc(radius, radius, radius / 2, 0, Math.PI * 2);
imageCtx.fill();
} else {
var img = Math.floor(Math.random() * loadedImage.length);
imageCtx.canvas.width = loadedImage[img].width;
imageCtx.canvas.height = loadedImage[img].height;
imageCtx.drawImage(loadedImage[img], 0, 0);
}
return parseImageData(imageCtx);
};
// init our objects and shapes
var init = function() {
var i;
for (i = 0; i < 30; i++) {
images.push(randomEgg());
}
for (i = 0; i < 10000; i++) {
objects.push({
angle: Math.random() * 360,
x: 100 + (Math.random() * mainCanvas.width / 2),
y: 100 + (Math.random() * mainCanvas.height / 2),
speed: 1 + Math.random() * 20,
image: Math.floor(Math.random() * (images.length))
});
}
loop();
};
var loop = function() {
draw();
requestAnimationFrame(loop);
};
// were our offsite images will be stored
var loadedImage = [];
(function preloadImages() {
var toLoad = ['https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png',
'https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png'
];
for (var i = 0; i < toLoad.length; i++) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
loadedImage.push(this);
if (loadedImage.length === toLoad.length) {
init();
}
};
img.src = toLoad[i];
}
})();
请注意,您要绘制的图像越大,绘图速度就越慢。
我正在尝试复制描述的方法 here on Whosebug。 但是我遇到了一些不知道如何解决的问题。
我设置jsfiddle来演示一切。 这是 second jsfiddle 只有粒子在移动和被绘制。
我的问题在于绘图,探查器显示大约 10000 个粒子 drawImage 占用了总循环时间的 40%。没有直接画图,只有计算,没有什么阻碍代码执行所以问题出在画图上。
有没有办法在没有这些副作用的情况下使用这种技术?目前,我向您展示了我如何使用圆弧创建圆形区域,但我也将 png 文件用于其他一些对象,它们表现出完全相同的行为。
(问题:黑色重叠区域不是透明区域,通过上面的圆可以看到底部圆的边缘)
我希望我表达得尽可能清楚(上图很清楚地显示了我的问题),非常感谢您的帮助。
绘制函数 - 最终绘制可见 canvas。
Game.prototype.draw2 = function(interpolation, canvas, ctx, group)
{
var canvasData = ctx.createImageData(canvas.width, canvas.height),
cData = canvasData.data;
for (var i = 0; i < group.length; i++)
{
var obj = group[i];
if(!obj.draw)
{
continue;
}
var imagePixelData = obj.imagePixelData;
var x = obj.previous.x + (obj.x - obj.previous.x) * interpolation;
var y = obj.previous.y + (obj.y - obj.previous.y) * interpolation;
for (var w = 0; w < obj.width; w++)
{
for (var h = 0; h < obj.height; h++)
{
if (x + w < canvas.width && obj.x + w > 0 &&
y + h > 0 && y + h < canvas.height)
{
var iData = (h * obj.width + w) * 4;
var pData = (~~ (x + w) + ~~ (y + h) * canvas.width) * 4;
cData[pData] = imagePixelData[iData];
cData[pData + 1] = imagePixelData[iData + 1];
cData[pData + 2] = imagePixelData[iData + 2];
if (cData[pData + 3] < 100)
{
cData[pData + 3] = imagePixelData[iData + 3];
}
}
}
}
}
ctx.putImageData(canvasData, 0, 0);
};
下面是我在其他不可见的地方准备粉红色圆形区域的方法canvas。
Game.prototype.constructors.Attractor.prototype.getImageData = function(context)
{
this.separateScene = new context.constructors.Graphics(this.width, this.height, false);
this.image = this.separateScene.canvas;
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.radius, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = '#ff9b9b';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.radiusCut, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = 'rgba(255, 255, 255, 0.27)';
this.separateScene.ctx.fill();
this.separateScene.ctx.beginPath();
this.separateScene.ctx.arc(this.radius, this.radius, this.coreRadius, 0, 2 * Math.PI, false);
this.separateScene.ctx.fillStyle = '#ff64b2';
this.separateScene.ctx.fill();
this.imageData = this.separateScene.ctx.getImageData(0, 0, this.width, this.height);
this.imagePixelData = this.imageData.data;
};
寻找黑色像素
@Loktar 的最佳答案是针对特定图像做出的,该图像仅由黑色和透明像素组成。
在imageData中,这两种类型的像素非常相似,只是它们的alpha值不同。所以他的代码只是对 alpha 值(每个循环中的第四个)进行 should-draw 检查。
cData[pData] = imagePixData[iData]; cData[pData + 1] = imagePixData[iData + 1]; cData[pData + 2] = imagePixData[iData + 2]; // only checking for the alpha value... if(cData[pData + 3] < 100){ cData[pData + 3] = imagePixData[iData + 3]; }
另一方面,您正在处理彩色图像。因此,当这部分针对透明像素执行时,并且您在此位置已经有一个彩色像素,前三行会将现有像素转换为透明像素的 rgb 值(0,0,0
)但保留 alpha 值现有像素(在您的情况下 255
)。
然后您会得到一个黑色像素,而不是之前此处的彩色像素。
要解决它,您可以将整个块包裹在检查当前 imagePixData
的不透明度的条件中,而不是检查已经绘制的块。
if (imagePixelData[iData+3]>150){
cData[pData] = imagePixelData[iData];
cData[pData + 1] = imagePixelData[iData + 1];
cData[pData + 2] = imagePixelData[iData + 2];
cData[pData + 3] = imagePixelData[iData + 3];
}
对抗白人
那些白色像素在这里是因为 anti-aliasing。它已经存在于@Loktar 的原始示例中,只是由于他的图像大小而不太明显。
当您处理 imageData 时,这些伪影是垃圾,因为我们只能修改每个像素,而我们不能在子像素上设置值。换句话说,我们不能使用抗锯齿。
这就是原始检查中的 <100
或我上面的解决方案中的 >150
的目的。
您在此检查中针对 alpha 值采用的最小范围,您获得的伪影越少。但另一方面,你的边框会越粗糙。
你必须自己找到正确的值,但圆圈是最差的,因为几乎每个边框像素都会被消除锯齿。
改进真棒(a.k.a我可以给你10000张彩图)
你的实际实施让我想到了可以对@Loktar 的解决方案进行一些改进。
不是保留原始图像的数据,我们可以对每个像素进行第一次循环,并存储一个新的 imageData 数组,该数组由六个槽组成:[x, y, r, g, b ,a]
。
这样,我们可以避免存储所有不需要的透明像素,从而减少每次调用的迭代次数,而且我们还可以避免在每次循环中进行任何 alpha 检查。最后,我们甚至不需要 "get the position pixel from the image canvas" 因为我们为每个像素存储了它。
这是一个带注释的代码示例作为概念证明。
var parseImageData = function(ctx) {
var pixelArr = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
// the width of our image
var w = ctx.canvas.width;
// first store our image's dimension
var filtered = [];
// loop through all our image's pixels
for (var i = 0; i < pixelArr.length; i += 4) {
// we don't want traparent or almost transparent pixels
if (pixelArr[i + 3] < 250) {
continue;
}
// get the actual x y position of our pixel
var f = (i / 4) / w;
var y = Math.floor(f);
var x = Math.round((f - y) * w);
// add the pixel to our array, with its x y positions
filtered.push(x, y, pixelArr[i], pixelArr[i + 1], pixelArr[i + 2], pixelArr[i + 3]);
}
return filtered;
};
// here we will store all our pixel arrays
var images = [];
// here we will store our entities
var objects = [];
var draw = function() {
// create a new empty imageData of our main canvas
var imageData = mainCtx.createImageData(mainCanvas.width, mainCanvas.height);
// get the array we'll write onto
var pixels = imageData.data;
var width = mainCanvas.width;
var pixelArray,
deg = Math.PI / 180; // micro-optimizaion doesn't hurt
for (var n = 0; n < objects.length; n++) {
var entity = objects[n],
// HERE update your objects
// some fancy things by OP
velY = Math.cos(entity.angle * deg) * entity.speed,
velX = Math.sin(entity.angle * deg) * entity.speed;
entity.x += velX;
entity.y -= velY;
entity.angle++;
// END update
// retrieve the pixel array we created before
pixelArray = images[entity.image];
// loop through our pixel Array
for (var p = 0; p < pixelArray.length; p += 6) {
// retrieve the x and positions of our pixel, relative to its original image
var x = pixelArray[p];
var y = pixelArray[p + 1];
// get the position of our ( pixel + object ) relative to the canvas size
var pData = (~~(entity.x + x) + ~~(entity.y + y) * width) * 4
// draw our pixel
pixels[pData] = pixelArray[p + 2];
pixels[pData + 1] = pixelArray[p + 3];
pixels[pData + 2] = pixelArray[p + 4];
pixels[pData + 3] = pixelArray[p + 5];
}
}
// everything is here, put the image data
mainCtx.putImageData(imageData, 0, 0);
};
var mainCanvas = document.createElement('canvas');
var mainCtx = mainCanvas.getContext('2d');
mainCanvas.width = 800;
mainCanvas.height = 600;
document.body.appendChild(mainCanvas);
// just for the demo
var colors = ['lightblue', 'orange', 'lightgreen', 'pink'];
// the canvas that will be used to draw all our images and get their dataImage
var imageCtx = document.createElement('canvas').getContext('2d');
// draw a random image
var randomEgg = function() {
if (Math.random() < .8) {
var radius = Math.random() * 25 + 1;
var c = Math.floor(Math.random() * colors.length);
var c1 = (c + Math.ceil(Math.random() * (colors.length - 1))) % (colors.length);
imageCtx.canvas.width = imageCtx.canvas.height = radius * 2 + 3;
imageCtx.beginPath();
imageCtx.fillStyle = colors[c];
imageCtx.arc(radius, radius, radius, 0, Math.PI * 2);
imageCtx.fill();
imageCtx.beginPath();
imageCtx.fillStyle = colors[c1];
imageCtx.arc(radius, radius, radius / 2, 0, Math.PI * 2);
imageCtx.fill();
} else {
var img = Math.floor(Math.random() * loadedImage.length);
imageCtx.canvas.width = loadedImage[img].width;
imageCtx.canvas.height = loadedImage[img].height;
imageCtx.drawImage(loadedImage[img], 0, 0);
}
return parseImageData(imageCtx);
};
// init our objects and shapes
var init = function() {
var i;
for (i = 0; i < 30; i++) {
images.push(randomEgg());
}
for (i = 0; i < 10000; i++) {
objects.push({
angle: Math.random() * 360,
x: 100 + (Math.random() * mainCanvas.width / 2),
y: 100 + (Math.random() * mainCanvas.height / 2),
speed: 1 + Math.random() * 20,
image: Math.floor(Math.random() * (images.length))
});
}
loop();
};
var loop = function() {
draw();
requestAnimationFrame(loop);
};
// were our offsite images will be stored
var loadedImage = [];
(function preloadImages() {
var toLoad = ['https://dl.dropboxusercontent.com/s/4e90e48s5vtmfbd/aaa.png',
'https://dl.dropboxusercontent.com/s/rumlhyme6s5f8pt/ABC.png'
];
for (var i = 0; i < toLoad.length; i++) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
loadedImage.push(this);
if (loadedImage.length === toLoad.length) {
init();
}
};
img.src = toLoad[i];
}
})();
请注意,您要绘制的图像越大,绘图速度就越慢。