Question

for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [Répartition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.

Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :

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

Then I switched to this function :

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

You can find the whole source here

I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).

I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).

Was it helpful?

Solution

I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.

The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.

I did a jsfiddle, and here's the result with 4 data points :

enter image description here

fiddle is here : 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
}

OTHER TIPS

In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.

For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):

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

Am I doing something wrong?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top