【问题标题】:Canvas: draw lots of elements with a changing gradient (emulate angular gradient)画布:绘制大量渐变渐变的元素(模拟角度渐变)
【发布时间】:2014-03-24 22:43:37
【问题描述】:

对于这个项目http://biduleohm.free.fr/ledohm/(对不起,用户界面是法语,但代码是英语)我需要一个角度渐变,但它在本机中不存在,所以我使用线性渐变实现了它线,我画的线越来越长,形成一个三角形。结果在图形上还可以,但速度不是很好(125 个三角形为 1850 毫秒)。它在 [Répartition] 选项卡中,如果其中一个输入上有 keyup 事件,它会重绘,不要害怕明显的缓慢,我限制为每 2000 毫秒最多重绘一次。

之前我在整个三角形上使用了简单的线性渐变(但这与现实不符)并且速度还可以,不到一秒就可以绘制出数千个三角形。使用了这个函数:

drawFrontLightForColor : function(x, y, w, h, color) {
    var x2 = x - w;
    var x3 = x + w;
    var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
    gradient.addColorStop(0,   'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
    gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
    gradient.addColorStop(1,   'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
    Distri.frontCanvas.fillStyle = gradient;
    Distri.frontCanvas.beginPath();
    Distri.frontCanvas.moveTo(x, y);
    Distri.frontCanvas.lineTo(x2, (y + h));
    Distri.frontCanvas.lineTo(x3, (y + h));
    Distri.frontCanvas.lineTo(x, y);
    Distri.frontCanvas.fill();
    Distri.frontCanvas.closePath();
},

然后我切换到这个功能:

drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
    var ratio = w / h;
    var tmpY;
    var tmpW;
    var x2;
    var x3;
    var gradient;
    Distri.frontCanvas.lineWidth = 1;
    for (var tmpH = 0; tmpH < h; tmpH++) {
        tmpY = y + tmpH;
        tmpW = Math.round(tmpH * ratio);
        x2 = x - tmpW;
        x3 = x + tmpW;
        gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
        gradient.addColorStop(0, edgeColor);
        gradient.addColorStop(0.5, centerColor);
        gradient.addColorStop(1, edgeColor);
        Distri.frontCanvas.beginPath();
        Distri.frontCanvas.moveTo(x2, tmpY);
        Distri.frontCanvas.lineTo(x, tmpY);
        Distri.frontCanvas.lineTo(x3, tmpY);
        Distri.frontCanvas.strokeStyle = gradient;
        Distri.frontCanvas.stroke();
        Distri.frontCanvas.closePath();
    }
},

你可以找到整个源码here

我不能将 beginPath、stroke、closePath 放在循环之外,因为每次迭代都会改变渐变(我已经尝试过,但它对每一行都使用了最后一个渐变(具有讽刺意味的是,它与第一个功能...)这是可以理解的,但不是我想要的)。

我接受任何建议(包括重做整个函数并修改他的调用者以外包一些代码)以提高速度,比如说 5 倍(最好是更多)。

【问题讨论】:

  • 你可以试试WebGL。它应该能够处理数百万个三角形。
  • 我没有在您的链接上找到“重新分区”选项卡。您能否将 .png 附加到您的问题中。
  • 是的,但如果可能的话,我想保留画布。在我看来,WebGL 似乎与所有浏览器都不兼容,但我会检查一下,谢谢。 @markE 我不能发布超过 2 个链接,因为这是我的第一个问题。但是执行 Shared.$tabs._1.click();在控制台中。
  • 嗯,WebGL 似乎比我想象的更有吸引力,3D 和交互性为我的项目提供了无限的可能性......并且所有最近的浏览器都支持它。谢谢推荐,我试试。但只是出于好奇,是否可以显着优化我的代码?

标签: javascript optimization canvas gradient


【解决方案1】:

我认为你从一开始就采取了错误的方式:当进行如此多的颜色变化时,你最好在像素级别进行操作。
所以是的,这可能与 webgl 像素着色器一起使用,但您必须努力让样板在所有平台上正常运行(或获取一个库来为您执行此操作)。
无论如何,有一个完美的解决方案可以满足您的需求,而且速度足够快(几毫秒):使用原始像素数据,使用相关函数一一更新,然后绘制结果。

这样做的步骤是:
- 创建一个与画布相同大小的缓冲区。
- 遍历它的像素,跟踪点的 x、y。
- 标准化坐标,使其与您的“空间”相匹配。
- 从您拥有的所有数据中计算归一化 (x,y) 的值。
- 从该值中写出一种颜色(在我的示例中,我选择灰度)。
- 将整个缓冲区绘制到画布上。

我做了一个 jsfiddle,这是 4 个数据点的结果:

小提琴在这里: http://jsfiddle.net/gamealchemist/KsM9c/3/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
    height = canvas.height;

// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
    var width = targetCanvas.width;
    var height = targetCanvas.height;
    var tempImg = ctx.createImageData(width, height);
    var buffer = tempImg.data;
    var offset = 0;
    var xy = [0, 0];
    function normalizeXY(xy) {
        xy[0] = xy[0] / width ;
        xy[1] = xy[1] / height;
    }
    for (var y = 0; y < height; y++)
    for (var x = 0; x < width; x++, offset += 4) {
        xy[0] = x; xy[1]=y;
        normalizeXY(xy);
        var val = Math.floor(valueForXY(xy, someData) * 255);
        buffer[offset] = val;
        buffer[offset + 1] = val;
        buffer[offset + 2] = val;
        buffer[offset + 3] = 255;
    }
    ctx.putImageData(tempImg, 0, 0);
}


// return normalized (0->1) value for x,y and
//      provided data.
//    xy is a 2 elements array
function someValueForXY(xy, someData) {
    var res = 0;
    for (var i = 0; i < someData.length; i++) {
        var thisData = someData[i];
        var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
        localRes = 0.04 * dist;
        res += localRes;
    }
    if (res > 1) res = 1;
    return res;
}

var someData = [
    [0.6, 0.2],
    [0.35, 0.8],
    [0.2, 0.5],
    [0.6, 0.75]
];

buildImage(canvas, someValueForXY, someData);

// ------------------------
function sq(x) {
    return x * x
}

【讨论】:

  • 非常聪明,谢谢。我将把它用于现有的画布(此外它允许使用任何公式来淡出光,我的线性淡出不是很现实......)我将添加第三个带有 WebGL 3D 模拟的画布。还有一个小问题:多种颜色的真实混合只是每个组件的所有来源的值相加吗?还是更复杂? (可能是二次的?)
  • 不客气。对于您的 webgl 想法,如果您愿意,可以使用它,但如果 imageData 解决方案不够好(对于 30 fps 而不是全屏画布,我很确定它是),请务必在之前进行基准测试。特别是因为 webgl 不擅长在对象内迭代,所以你的像素着色器会很大而且很慢(但肯定会更快)。对于混合,最容易的是将 r,g,b 分开以清楚地看到事物。所以是的,基本方法是一个补充,但你可能更喜欢其他一些混合物......在这里说太多了!好机会!
  • 事实上,画布仅在输入中的更改事件时刷新,并且在延迟 2 秒后(如果用户不断更改参数则无需刷新......)不需要运行许多fps,所以解决方案可能已经足够好了,我会先在许多光源上进行测试,然后才能确定。我对 WebGL 有一些问题,但我会为此提出另一个主题,因为它与这个问题无关。再来一次谢谢 :)
【解决方案2】:

事实上,GameAlchemist 的解决方案并不快,或者我做错了什么。我只为顶视图实现了这个算法,因为前视图要复杂得多。

对于 120 盏灯,顶视图使用旧代码需要 100-105 毫秒,而使用此代码需要 1650-1700 毫秒(此外,新代码中仍然缺少一些东西,例如颜色):

drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
    function sq(x) {
        return x * x;
    }
    var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
    var rawData = tmpImg.data;
    var ledsArray = ledsArrays[color];
    var len = ledsArray.length;
    var i = 0;
    for (var y = 0; y < canvasD; y++) {
        for (var x = 0; x < canvasW; x++, i += 4) {
            var intensity = 0;
            for (var j = 0; j < len; j++) {
                intensity += 2 * Math.pow(
                    sq((rampX + ledsArray[j].x) - x) +
                    sq((rampZ + ledsArray[j].y) - y),
                    -0.5
                );
            }
            if (intensity > 1) {
                intensity = 1;
            }
            intensity = Math.round(intensity * 255);
            rawData[i] = intensity;
            rawData[i + 1] = intensity;
            rawData[i + 2] = intensity;
            rawData[i + 3] = 255;
        }
    }
    Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},

我做错了吗?

【讨论】:

    猜你喜欢
    • 2014-04-09
    • 1970-01-01
    • 2018-04-17
    • 2011-04-29
    • 1970-01-01
    • 1970-01-01
    • 2011-01-28
    • 1970-01-01
    相关资源
    最近更新 更多