1

I have trying to render an image in the browser which is built like this:

  • A bunch of rectangles are each filled with a radial gradient (ideally Gaussian, but can be approximated with a few stopping points
  • Each rectangle is rotated and translated before being deposited on a drawing area

  • The image is flattened by summing all the intensities of the rectangles (and cropping to the drawing area's dimensions )

  • The intensity is rescaled so that the highest intensity is 255 and the lowest 0 (ideally I can apply some sort of gamma correction too)

  • Finally an image is drawn where the color of each pixel is taken from a palette of 256 colors.

The reason I cannot do this easily with a canvas object is that I need to be working in floating points or I'll lose precision. I do not know in advance what the maximum intensity and minimum intensity will be, so I cannot merely draw transparent rectangles and hope for the best.

Is there a way to do this in webgl? If so, how would I go about it?

Arthur B.
  • 3,445
  • 3
  • 21
  • 24

1 Answers1

-1

You can use the regular canvas to perform this task :

1) check min/max of your rects, so you can build a mapping function double -> [0-255] out of that range.

2) draw the rects in 'lighter' mode == add the component values.

3) you might have a saturation when several rects overlaps : if so, double the mapping range and go to 2).
Now if you don't have saturation just adjust the range to use the full [0-255] range of the canvas, and you're done.

Since this algorithm makes use of getImageData, it might not reach 60 fps on all browsers/devices. But more than 10fps on desktop/Chrome seems perfectly possible.

Hopefully the code below will clarify my description :

//noprotect
// boilerplate
var cv = document.getElementById('cv');
var ctx = cv.getContext('2d');

// rectangle collection
var rectCount = 30;
var rects = buildRandRects(rectCount);


iterateToMax();


// --------------------------------------------

function iterateToMax() {
    var limit = 10; // loop protection
    // initialize min/max mapping based on rects min/max
    updateMapping(rects);
    //
    while (true) {
        // draw the scene using current mapping
        drawScene();
        // get the max int value from the canvas
        var max = getMax();
        if (max == 255) {
            // saturation ?? double the min-max interval
            globalMax = globalMin + 2 * (globalMax - globalMin);
        } else {
            // no sauration ? Just adjust the min-max interval
            globalMax = globalMin + (max / 255) * (globalMax - globalMin);
            drawScene();
            return;
        }
        limit--;
        if (limit <= 0) return;
    }
}

// --------------------------------------------
// --------------------------------------------

// Oriented rectangle Class.
function Rect(x, y, w, h, rotation, min, max) {
    this.min = min;
    this.max = max;
    this.draw = function () {
        ctx.save();
        ctx.fillStyle = createRadialGradient(min, max);
        ctx.translate(x, y);
        ctx.rotate(rotation);
        ctx.scale(w, h);
        ctx.fillRect(-1, -1, 2, 2);
        ctx.restore();
    };
    var that = this;

    function createRadialGradient(min, max) {
        var gd = ctx.createRadialGradient(0, 0, 0, 0, 0, 1);
        var start = map(that.min);
        var end = map(that.max);
        gd.addColorStop(0, 'rgb(' + start + ',' + start + ',' + start + ')');
        gd.addColorStop(1, 'rgb(' + end + ',' + end + ',' + end + ')');
        return gd;
    }
}

// Mapping : float value -> 0-255 value
var globalMin = 0;
var globalMax = 0;

function map(value) {
    return 0 | (255 * (value - globalMin) / (globalMax - globalMin));
}

// create initial mapping 
function updateMapping(rects) {
    globalMin = rects[0].min;
    globalMax = rects[0].max;
    for (var i = 1; i < rects.length; i++) {
        var thisRect = rects[i];
        if (thisRect.min < globalMin) globalMin = thisRect.min;
        if (thisRect.max > globalMax) globalMax = thisRect.max;
    }
}

// Random rect collection
function buildRandRects(rectCount) {
    var rects = [];
    for (var i = 0; i < rectCount; i++) {
        var thisMin = Math.random() * 1000;
        var newRect = new Rect(Math.random() * 400, Math.random() * 400, 10 + Math.random() * 50, 10 + Math.random() * 50, Math.random() * 2 * Math.PI, thisMin, thisMin + Math.random() * 1000);
        rects.push(newRect);
    }
    return rects;
}

// draw all rects in 'lighter' mode (=sum values)
function drawScene() {
    ctx.save();
    ctx.globalCompositeOperation = 'source-over';
    ctx.clearRect(0, 0, cv.width, cv.height);
    ctx.globalCompositeOperation = 'lighter';
    for (var i = 0; i < rectCount; i++) {
        var thisRect = rects[i];
        thisRect.draw();
    }
    ctx.restore();
}


// get maximum value for r for this canvas 
//   ( == max r, g, b value for a gray-only drawing. )
function getMax() {
    var data = ctx.getImageData(0, 0, cv.width, cv.height).data;
    var max = 0;
    for (var i = 0; i < data.length; i += 4) {
        if (data[i] > max) max = data[i];
        if (max == 255) return 255;
    }
    return max;
}
<canvas id='cv' width = 400 height = 400></canvas>
GameAlchemist
  • 18,995
  • 7
  • 36
  • 59
  • Thanks for your answer and the code, but how do you deal with numerical stability? What if I have 1,000 overlapping rectangles, each contributing a small amount? – Arthur B. May 15 '15 at 18:33
  • You're welcome. Is it just an academical 'what if', or is it a real possibility ? (in other words : what is the maximum rect count / dynamic range your app is supposed to have ?) – GameAlchemist May 15 '15 at 19:13
  • No I do expect to have on the order of a few thousand overlapping rectangles over every single pixel – Arthur B. May 15 '15 at 19:26
  • Then i'm tempted to delete my answer... You should either a) use a float webGL buffer (ouch ! what an overhead just to get the first rect to display ! not mentioning browser support issue.) OR b) draw by hand your gradient rects in a Float32 Array => convert to an ImageData => draw on a canvas. Maybe it's because i'm more fluent in Context2D, but i'd bet i'd do b) in 4 hours, and a) in 4... days !!! – GameAlchemist May 15 '15 at 19:54
  • Right more I'm drawing everything pixel by pixel in JS, but it takes me on the order of 300ms just to fill a 512x512 image with a uniform color... – Arthur B. May 15 '15 at 20:56
  • I wonder if you couldn't vastly improve the js rendering time. Re-using the Float32 buffer and the imageData is one way, but more generally it could be interesting to post a stripped down version of your code on codereview to get advices on optimisations. – GameAlchemist May 16 '15 at 07:58
  • I'm going to try to use your code but by sorting rectangles in intensity and pairing up the terms in order to avoid numerical instability. Yikes. – Arthur B. May 16 '15 at 23:09