【问题标题】:I'm having troubles with copying transparent pixels with canvas putImageData from another canvas/image(png)我在从另一个画布/图像(png)使用画布 putImageData 复制透明像素时遇到问题
【发布时间】:2016-07-08 12:23:06
【问题描述】:

我正在尝试复制here on stackoverflow 中描述的方法。 但是我遇到了一些我不知道如何解决的问题。

我设置了jsfiddle 来演示一切。 这是second jsfiddle,只有粒子在移动和被绘制。

我的问题在于绘图,分析器显示大约 10000 个粒子 drawImage 占用了总循环时间的 40%。不直接画图,只做计算,不妨碍代码执行,所以问题出在画图上。


有没有办法在没有这些副作用的情况下使用这种技术?目前,我向您展示了如何使用圆弧创建圆形区域,但我也将 png 文件用于其他一些对象,它们表现出完全相同的行为。

(问题:黑色重叠区域而不是透明区域,通过上面的圆圈可以看到底部圆圈的边缘)

我希望我尽可能清楚地表达自己(上图非常清楚地显示了我的问题),感谢您的帮助。

绘图功能 - 最终绘制到可见画布。

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);
};

这就是我如何在其他不可见的画布上准备粉红色的圆形区域。

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;
};

【问题讨论】:

  • 您的问题太大,任何人都无法回答。我们无法通过您的 500 行代码来查看您做错了什么,最好在每个帖子中问一个问题。当您有 10000 多个要绘制的对象时,您链接的帖子非常适合。在 5000 以下,drawImage 在我测试过的大多数机器上都更快,所以我什至不确定你是否需要它。你的原始图像越大,这个解决方案就会变得越慢。 &lt;100 用于检查像素是否透明。超过这个阈值,我们可以认为它是一个抗锯齿伪影,我们不想要它。
  • 我删除了额外的信息,只保留了主要问题。谢谢你。顺便说一句,我正在使用更多数量的粒子(屏幕上大约 10k)。使用 chrome profiler,我将问题减少到 drawImage() 函数,它在运行整个代码时占用了总时间的 40%。我的绘图功能没有 500 行,其余的与问题本身并没有真正的联系。
  • 我又添加了一个只有粒子的小提琴。我添加了一种停止循环并启动它的方法。第二个小提琴显示了与加载的 png 图像相同的问题,该图像首先被绘制到单独的画布中,然后来自该画布的 imageData 用于绘制到可见画布上。除了我已经在 jsfiddle 中所做的之外,我无法提供任何其他行,因为我已经注释掉了其他所有内容然后只画。我在单独的画布上绘制 FPS (...)。
  • 就在一分钟前,我设法通过在每次绘制期间手动过滤像素来消除黑色效果,但对于程序生成的动态图形,我显然无法做到这一点。如果需要任何更改,我希望我以正确的方式为您调整了 jsfiddle,请让我知道我正在尽力调整一切以使其更友好。
  • 好的,找到了,因为 Loktar 只检查黑色像素,所以仅 alpha 检查就足够了。您需要将整个像素操作块包装在if(imagePixelData[iData]+imagePixelData[iData + 1]+imagePixelData[iData + 2]+imagePixelData[iData + 3]&gt;0) 中并删除if (cData[pData + 3] &lt; 100) 行。正如在另一个答案中一样,它仍然会将 r、g 和 b 值更改为我们所在的像素。这不是黑色的问题。更新小提琴:jsfiddle.net/3tLzpLyd/7 很抱歉之前误导了 cmets。

标签: javascript html canvas putimagedata


【解决方案1】:

寻找黑色像素


@Loktar 的最佳答案是针对特定图像制作的,仅由黑色和透明像素组成。

在 imageData 中,这两种类型的像素非常相似,因为只有它们的 alpha 值不同。所以他的代码只是对 alpha 值(每个循环中的第四个)进行了一次应该绘制检查。

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 时,这些工件是垃圾,因为我们只能修改每个像素,而我们不能在子像素上设置值。换句话说,我们不能使用抗锯齿。

这就是&lt;100 在原始检查中的目的,或者&gt;150 在我上面的解决方案中的目的。

您在此检查中对 alpha 值的范围越小,得到的伪影就越少。但另一方面,你的边界会越粗糙。

你必须自己找到正确的值,但圆圈是最糟糕的,因为几乎每个边框像素都会被消除锯齿。

改进令人敬畏的 (我可以给你 10000 张彩色图像)


您的实际实现让我想到了可以对@Loktar 的解决方案进行的一些改进。

我们可以不保留原始图像的数据,而是对每个像素进行第一次循环,并存储一个新的 imageData 数组,该数组由六个插槽组成:[x, y, r, g, b ,a]

这样,我们可以避免存储所有不需要的透明像素,从而减少每次调用的迭代次数,并且我们还可以避免在每个循环中进行任何 alpha 检查。最后,我们甚至不需要“从图像画布中获取位置像素”,因为我们为每个像素存储了它。

这是一个带注释的代码示例,作为概念证明。

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];
  }
})();

请注意,要绘制的图像越大,绘制速度也会越慢。

【讨论】:

  • 谢谢,感谢所有帮助。我设法用我的代码对不低于 30 fps 的 30000 个粒子进行了压力测试,我认为这是一个很好的结果(我的游戏中从来没有得到 30000 个粒子 :-))和 20000 个粒子,50 到60 fps - 而所有粒子在所有情况下都与其他对象交互。但是那种颠簸的运动让我很生气,当你使用较大的物体快速移动而不是在相似的方向上时,它并没有那么糟糕,但是对于以相似的速度在相似的方向上移动的较小的物体来说,它是一个真正的美学杀手。 :-)
  • 我正在考虑用我自己的方式来解决这个问题。我可以渲染 20000 个粒子,这远远超出了我的需要。因此,考虑到这一点,我认为可能不止一次添加一个带有偏移量的粒子(或者更准确地说,添加以仅渲染偏移部分)可以帮助减少整数步移动带来的颠簸感 - 而偏移部分的 alpah 值较低所以它不是完全可见的,到目前为止我的测试表明它极大地增强了体验,同时保持游戏中的高粒子数。但是我需要相当长的时间才能对所有的故障进行微调:-)。
  • 对于您的尾随,我认为您最好使用 drawImage :在一组上下文中缓冲整个画布的十帧,并使用 globalCompositeOperation 设置为 @ 绘制缓冲图像987654333@,每次抽奖时减少globalAlpha
猜你喜欢
  • 2011-07-09
  • 1970-01-01
  • 2012-03-02
  • 1970-01-01
  • 1970-01-01
  • 2012-04-18
  • 1970-01-01
  • 1970-01-01
  • 2011-02-24
相关资源
最近更新 更多