Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Canvas using Uint32Array: Wrong colors are being rendered

I'm currently creating a JS canvas where I want to display a box of different colors.

I'm using uint32 for extra speed, and my colors never display correctly! I've looked at the examples mainly over here: https://stackoverflow.com/a/19502117 where someone said in the comments:

(small I or JS will throw an error). Tip for OP: colors for Uint32 can also be given simply be using hex - no need to do shifting: 0xff00000 = black + alpha set to 255; for little-endian/LSB CPUs, opposite on big-endian/MSB CPUs."

I'm certain my laptop is little-endian.

I have a demo of my issue here: http://jsfiddle.net/GhwUC/357/

var canvas = document.getElementById('canvas');
var canvasWidth  = canvas.width;
var canvasHeight = canvas.height;
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);

var buf = new ArrayBuffer(imageData.data.length);
var buf8 = new Uint8ClampedArray(buf);
var data = new Uint32Array(buf);

for (var y = 0; y < canvasHeight; ++y) {
    for (var x = 0; x < canvasWidth; ++x) {
        data[y * canvasWidth + x] = 0xff80d7ff // Should be light blue (#80d7ff)
    }
}

imageData.data.set(buf8);

ctx.putImageData(imageData, 0, 0);

The color in question here is:

color

But the fiddle displays a yellow-ish color: color fail

It's the same on other colors, thanks a lot in advance!

EDIT: thanks @Oriol for quick answer! I used the following function to reverse my colors (in case anyone was interested):

function reverseUint32 (uint32) {
    var s32 = new Uint32Array(4);
    var s8 = new Uint8Array(s32.buffer);
    var t32 = new Uint32Array(4);
    var t8 = new Uint8Array(t32.buffer);        
    reverseUint32 = function (x) {
        s32[0] = x;
        t8[0] = s8[3];
        t8[1] = s8[2];
        t8[2] = s8[1];
        t8[3] = s8[0];
        return t32[0];
    }
    return reverseUint32(uint32);
};

Use it like: reverseUint32(0xfc66feff)

like image 356
Peter Willemsen Avatar asked Dec 10 '22 15:12

Peter Willemsen


2 Answers

This happens when you treat a Uint8Array buffer as a Uint32 in little endian:

var buf = new Uint8Array([0x12, 0x34, 0x56, 0x78]).buffer;
console.log(new Uint32Array(buf)[0].toString(16));
// "78563412" in little endian, "12345678" in big endian

So in little endian, the order becomes AABBGGRR instead of AARRGGBB.

You could reverse 0x80d7ffff to 0xffffd780, but then it wouldn't work on big endian machines.

To avoid these problems you can use a DataView, which allows to specify the endianness, defaulting to big endian:

view.setUint32(offset, 0xffffd780, true);  // #80d7ff, in little endian
view.setUint32(offset, 0x80d7ffff, false); // #80d7ff, in big endian

var canvas = document.getElementById('canvas'),
    canvasWidth  = canvas.width,
    canvasHeight = canvas.height,
    ctx = canvas.getContext('2d'),
    imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight),
    view = new DataView(imageData.data.buffer);
for (var y = 0; y < canvasHeight; ++y) {
  for (var x = 0; x < canvasWidth; ++x) {
    var offset = 4 * (y * canvasWidth + x);
    view.setUint32(offset, 0x80d7ffff); // light blue (#80d7ff)
  }
}
ctx.putImageData(imageData, 0, 0);
<canvas id="canvas" height="256" width="256"></canvas>

But it seems that browsers haven't optimized much DataView, so it's slow. Then it might be better to set the color components separately in the Uint8ClampedArray:

var canvas = document.getElementById('canvas'),
    canvasWidth  = canvas.width,
    canvasHeight = canvas.height,
    ctx = canvas.getContext('2d'),
    imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight),
    data = imageData.data;
for (var y = 0; y < canvasHeight; ++y) {
  for (var x = 0; x < canvasWidth; ++x) {
    var offset = 4 * (y * canvasWidth + x);
    // light blue (#80d7ff)
    data[offset+0] = 0x80; // red
    data[offset+1] = 0xd7; // green
    data[offset+2] = 0xff; // blue
    data[offset+3] = 0xff; // alpha
  }
}
ctx.putImageData(imageData, 0, 0);
<canvas id="canvas" height="256" width="256"></canvas>
like image 181
Oriol Avatar answered Feb 02 '23 21:02

Oriol


All about performance.

The best solution is to do an endian test.

var isLittleEndian = true;
(()=>{
    var buf = new ArrayBuffer(4);
    var buf8 = new Uint8ClampedArray(buf);
    var data = new Uint32Array(buf);
    data[0] = 0x0F000000;
    if(buf8[0] === 0x0f){
        isLittleEndian = false;
    }
})();

The write the pixels depending on the test.

var imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
var data = new Uint32Array(imageData.data.buffer);
var val = 0xffffd780;
if(isLittleEndian){
    val = 0x80d7ffff;
}
var i = 0;
while(i < data.length) {
    data[i++] = val; 
}

A look at performance

In the time it takes to write one value using (View) view.Uint32(i,val); you can write 33 values using (Direct) data[i] = val;

Stats from Firefox.

For constant cycle time test.

  • Direct writes * 10,000 per cycle : 23222 97.07% 100%.
  • View writes * 10,000 per cycle: 702 : 2.93% 3.02%

First % is percent of total. Second % is percent of fastest (Direct)

For performance time per 10,000 writes (time in 1/1,000,000 seconds)

  • Direct 1701 cycles * 10000 writes. Mean : 43.063 (1e-6 sec). Variance : 0.210
  • View 1701 cycles * 10000 writes. Mean : 1424.832 (1e-6 sec). Variance : 379.441

Note the high variance value for View this is due to Javascript optimisation. Doing short bursts of writes via View can be significantly slower.

For performance time per 10,000 writes (time in 1/1,000,000 seconds) allowing variance to stabilize.

  • Direct 31636 cycles * 10000 writes. Mean : 29.981 (1e-6 sec). Variance : 0.005
  • View 31802 cycles * 10000 writes. Mean : 982.195 (1e-6 sec). Variance : 0.154

More tests

As requested in comments some more tests. A test unit is a call to one of the test functions. Thus in the following tests 10000 32Bit writes per test unit.

Compare 8Bit writes to 32Bit writes

functions tested

  testFunctions = [{
          func:function(){
              for(i = 0; i < 40000; i ++){
                  data[i++] = 0xFF;
                  data[i++] = 0xFF;
                  data[i++] = 0xd7;
                  data[i] = 0x80;
              }},
          name:"8Bit",
      },{
          func:function(){
              for(i = 0; i < 10000; i ++){
                  view2[i] = 0x80d7ffff;
             }},
          name:"32Bit",
      }
  ];

Common context

var i
var i,arr;
var data = new Uint8ClampedArray(100000);
var view2 = new Uint32Array(data.buffer);

Test results raw dump.

Test complete Results.
Function name, Calls per sec, % cycle time, % of best time
32Bit : 33743 : 76.84% 100%.
8Bit : 10170 : 23.16% 30.14%

Total cycles : 1000
Stable cycles : 899 Total.
Tests per cycle : 660
Testing cycles stable for : 800 of 800 cycles 100.00%
Max test variance 0.000%
Test results are good.

List of all test function results.
Mean times in micro secs 1/1,000,000 times mark with ms in milliseconds 1/1,000
# calls, total time, mean time
--------------------
Test function : 8Bit
62264 tests 6122.825ms Mean : 98
61942 tests 6088.945ms Mean : 98
62283 tests 6124.810ms Mean : 98
62233 tests 6121.010ms Mean : 98
Variance : 0.000micro sec. normalised : 0.000%
Ran : 248722 over 24457.590ms Mean : 98.333micro sec
--------------------
Test function : 32Bit
62084 tests 1839.180ms Mean : 30
61738 tests 1829.285ms Mean : 30
62282 tests 1846.225ms Mean : 30
62390 tests 1849.650ms Mean : 30
Variance : 0.000micro sec. normalised : 0.000%
Ran : 248494 over 7364.340ms Mean : 29.636micro sec
Total number of tests run : 497216

The View and Direct write

Detailed view of a test run described at the beginning of this answer.

Functions and shared context

 sharedFunction = function(){ 
    var i;
    var data = new Uint8ClampedArray(100000);
    var view1 = new DataView(data.buffer);
    var view2 = new Uint32Array(data.buffer);
 }
  testFunctions = [{
          func:function(){
              for(i = 0; i < 10000; i ++){
                  view1.setUint32(i, 0x80d7ffff); 
              }            
          },
          name:"View",
      },{
          func:function(){
              for(i = 0; i < 10000; i ++){
                  view2[i] = 0x80d7ffff;
             }},
          name:"Direct",
      }
  ];

Results

Test complete Results.
Calls per sec, % cycle time, % of best time
Direct : 35766 : 97.07% 100%.
View : 1080 : 2.93% 3.02%

Total cycles : 1000
Stable cycles : 899 Total.
Tests per cycle : 73
Testing cycles stable for : 800 of 800 cycles 100.00%
Max test variance 5.231%
Test results are good.

Mean times in micro secs 1/1,000,000 times mark with ms in milliseconds 1/1,000
# calls, total time, mean time
--------------------
Test function : View
8583 tests 7850.680ms Mean : 915
8454 tests 7830.950ms Mean : 926
8201 tests 7639.375ms Mean : 932
8459 tests 7883.150ms Mean : 932
Variance : 48.445micro sec. normalised : 5.231%
Ran : 33697 over 31204.155ms Mean : 926.022micro sec
--------------------
Test function : Direct
8434 tests 235.295ms Mean : 28
8347 tests 234.190ms Mean : 28
8451 tests 237.045ms Mean : 28
8260 tests 229.900ms Mean : 28
Variance : 0.009micro sec. normalised : 0.033%
Ran : 33492 over 936.430ms Mean : 27.960micro sec
Total number of tests run : 67189

Note Each test function is run as 4 separate functions. A stable test is when all 4 current cycle test times match the previous test cycle times. Javascript optimization will cause times to vary and as there is no way to know for sure when optimisation is happening the testing code waits till all test functions return a stable time at least 100 cycles. Further cycle time instability will be shown in the variance values.

Tests per cycle is an average (not noted in results)

All test functions are run in random order via testFunction[ Math.floor(Math.random() * testFunctionCount * testsPerFunction) ]();

Timing is via performance.now(); and measures only the inner content of the test functions.

like image 27
Blindman67 Avatar answered Feb 02 '23 22:02

Blindman67