问题
I'm trying to copy method discribed here on stackoverflow. But I'm having some problems which I don't know how to solve.
I set up jsfiddle to demonstrate everything. Here is the second jsfiddle with only particles moving and being drawn.
My problem lies in drawing, profiler showed that with around 10000 particles drawImage takes 40% of overall loop time. Without drawing directly and only calculations nothing hinders code exectuion so problem lies in drawing.
Is there a way how to use this technique without these side effects? Currently I show you how I create circle areas with arc but I also use png files for some other objects and they exhibit exactle the same behaviour.
(problem: black overlapping area instead of transparent area, bottim circle's edge can be seen through the circle above)
I hope I expressed myself as clearly as possible (picture abovedisplays my problem very clearly) and I would like to thank you for your help.
Draw function - final draw to visible 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);
};
And here is how I prepare pinkish circular area in other invisible 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;
};
回答1:
Hunting the black pixels
@Loktar's great answer was made for a particular image, only composed of black and transparent pixels.
In the imageData, these two type of pixels are very similar, since only their alpha value differ. So his code was only doing a should-draw check over the alpha value (the fourth in each loop).
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]; }
You, in the other hand, are dealing with colored images. So when this part is executed against a transparent pixel, and that you already have a colored pixel at this position, the three first lines will convert the existing pixel to the transparent one's rgb values (0,0,0
) but leave the alpha value of the existing pixel (in your case 255
).
You then have a black pixel instead of the colored one that were here previously.
To solve it, you can wrap the full block in a condition that checks the opacity of the current imagePixData
, instead of checking the one already drawn.
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];
}
Fighting the white ones
Those white pixels are here because of the anti-aliasing. It was already there in @Loktar's original example, simply less visible because of the size of his images.
These artifacts are crap when you do deal with imageData, since we can just modify each pixel, and that we can't set values on sub-pixels. In other words, we can't make use of anti-aliasing.
That's the purpose of the <100
in original checking, or the >150
in my solution above.
The smallest range you will take in this check against the alpha value, the less artifacts you'll get. But in the other hand, the rougher your borders will be.
You ave to find the right value by yourself, but circles are the worst since almost every border pixels will be anti-aliased.
Improving the awesome (a.k.a I can get you 10000 colored images)
Your actual implementation made me think about some improvements that could be made on @Loktar's solution.
Instead of keeping the original image's data, we could do a first loop over every pixels, and store a new imageData array, composed of six slots : [x, y, r, g, b ,a]
.
This way, we can avoid the storing of all the transparent pixels we don't want, which makes less iterations at each call, and we can also avoid any alpha checking in each loop. Finally, we don't even need to "get the position pixel from the image canvas" since we stored it for each pixel.
Here is an annotated code example as a proof of concept.
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];
}
})();
Note that the bigger your images to draw will be, the slowest the drawings will be too.
来源:https://stackoverflow.com/questions/36140427/im-having-troubles-with-copying-transparent-pixels-with-canvas-putimagedata-fro