加速HTML5 Canvas像素渲染

6

我正在设计一款类似于Photoshop的基于HTML5 Canvas元素的Web应用程序。该程序在没有使用混合模式时运行良好且速度很快。但是,一旦加入混合模式,程序就会变得缓慢。我通过将每个画布元素合并成一个,并从底部画布开始使用正确的混合模式组合每个画布的每个像素来实现混合模式。

for (int i=0; i<width*height*4; i+=4) {
    var base = [layer[0][i],layer[0][i+1],layer[0][i+2],layer[0][i+3]];
    var nextLayerPixel = [layer[1][i],layer[1][i+1],layer[1][i+2],layer[1][i+3]];
    //Apply first blend between first and second layer
    basePixel = blend(base,nextLayerPixel);
    for(int j=0;j+1 != layer.length;j++){
        //Apply subsequent blends here to basePixel
        nextLayerPixel = [layer[j+1][i],layer[j+1][i+1],layer[j+1][i+2],layer[j+1][i+3]];
        basePixel = blend(basePixel,nextLayerPixel);
   }
   pixels[i] = base[0];
   pixels[i+1] = base[1];
   pixels[i+2] = base[2];
   pixels[i+3] = base[3];
}
canvas.getContext('2d').putImageData(imgData,x,y);

使用IT调用不同的混合模式。我的“正常”混合模式如下:

var blend = function(base,blend) {
    var fgAlpha = blend[3]/255;
    var bgAlpha = (1-blend[3]/255)*base[3]/255;
    blend[0] = (blend[0]*fgAlpha+base[0]*bgAlpha);
    blend[1] = (blend[1]*fgAlpha+base[1]*bgAlpha);
    blend[2] = (blend[2]*fgAlpha+base[2]*bgAlpha);
    blend[3] = ((blend[3]/255+base[3])-(blend[3]/255*base[3]))*255;
    return blend;
}

我的Chrome测试结果(在测试的浏览器中表现最佳)是在一个 620x385(238,700 像素)的画布上混合三层,大约需要 400ms。

这只是一个非常小的实现,因为大多数项目的规模都会更大,并且包含更多的图层,这将使这种方法下的执行时间急剧增加。

我想知道是否有更快的方法来结合两个带有混合模式的画布上下文,而无需逐个像素地处理。


nextLayerPixel是什么?你如何创建它,为什么要在blend函数(第二个参数)中更改它? - Bergi
我最初排除了那部分代码,以展示功能而不使其变得混乱,但现在我已经将其添加进去了。'nextLayerPixel'只是一个变量,它引用每个图层中相同的像素。因此,在一个有3个图层和像素x:30,y:20的项目中,它将获取底部图层像素30,20,然后是中间的30,20,最后是顶部的30,20。 - Evan Kennedy
2个回答

3
不要创建太多的4值数组,使用已经存在的内存会更快。此外,您可能想在layer数组上使用reduce函数,这似乎正是您需要的。然而,不使用任何函数可能会更快一些——不需要创建执行上下文。以下代码仅为每个图层调用混合函数,而不是每个像素*层数。
var layer = [...]; // an array of CanvasPixelArrays
var base = imgData.data; // the base CanvasPixelArray whose values will be changed
                         // if you don't have one, copy layer[0]
layer.reduce(blend, base); // returns the base, on which all layers are blended
canvas.getContext('2d').putImageData(imgData, x, y);

function blend(base, pixel) {
// blends the pixel array into the base array and returns base
    for (int i=0; i<width*height*4; i+=4) {
        var fgAlpha = pixel[i+3]/255,
            bgAlpha = (1-pixel[i+3]/255)*fgAlpha;
        base[i  ] = (pixel[i  ]*fgAlpha+base[i  ]*bgAlpha);
        base[i+1] = (pixel[i+1]*fgAlpha+base[i+1]*bgAlpha);
        base[i+2] = (pixel[i+2]*fgAlpha+base[i+2]*bgAlpha);
        base[i+3] = ((fgAlpha+base[i+3])-(fgAlpha*base[i+3]))*255;
//                           ^ this seems wrong, but I don't know how to fix it
    }
    return base;
}

替代方案:完全不要在JavaScript中混合图层。只需将您的画布绝对定位在彼此上方,并给它们一个CSS opacity。这应该大大加快显示速度。但我不确定是否与其他效果一起使用时会产生冲突,如果需要应用于多个图层,则可能无法正常工作。

非常好的答案。我花了一段时间阅读文档才让reduce函数与我的代码实际配合起来。我仍然需要想出一些解决方案,因为最快的浏览器速度只有180毫秒,而较慢的浏览器速度接近1000毫秒。移动浏览器超过1秒。这对我的应用程序来说是无法工作的,除非有另一种方法可以同时编辑更大的区域,否则我可能不得不将混合模式去掉。 - Evan Kennedy
你需要多频繁地调用这个混合函数? - Bergi
每当项目中的一个图层具有非正常模式时,混合函数就会被调用,只要其中一个图层被修改。它只会调用已修改的区域,但如果用户移动大型对象或使用大画笔进行绘画,则需要快速处理大面积,并且每次移动图形时都会被调用。这不仅仅是mousedown/mouseup。它还包括mousemove,每秒将被调用多次。如果我能够将其至少达到5fps(200ms),那么这可能会起作用,但我不认为在较慢的浏览器中会发生这种情况。 - Evan Kennedy
你使用多少层,这会有影响吗?你可以将除当前活动层外的所有层缩减为单像素数据数组,然后每次只将活动层混合到预先计算好的数组中。 - Bergi
这正是我在想的。唯一的问题是,我不知道如何计算从顶层开始的混合模式。所以我只能计算当前图层下面所有图层的混合效果。因为混合效果依赖于当前图层下面的所有图层,所以只有当被修改的图层是较高层级之一时才有帮助。而且图层的数量可能会根据项目的不同而变化。 - Evan Kennedy
好的,如果混合函数确实不是可结合的,您可以尝试使用堆叠画布方法(请参见我的编辑)。 - Bergi

2
传统上,这种大规模的像素操作是通过在GPU上运行而不是在CPU上加速的。不幸的是,canvas不支持此功能,但您可以使用SVG Filters实现潜在的解决方法。这将允许您使用硬件加速的混合模式(feBlend)将两个图像混合在一起。如果您将图层渲染为两个图像,然后在SVG中引用这些图像,就可以使其正常工作。
这里有一个很好的插图概述如何工作: http://blogs.msdn.com/b/ie/archive/2011/10/14/svg-filter-effects-in-ie10.aspx(适用于支持SVG Filters的任何浏览器,包括IE10)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接