Ed Lynch
Ed Lynch

Reputation: 623

2D array to ImageData

So I have a 2D array created like this:

//Fill screen as blank
for(var x = 0; x<500; x++ ){
    screen[x] = [];
    for(var y = 0; y<500; y++ ){
        screen[x][y] = '#ffffff';
    }
}

And was wondering if there's an easy way to convert it to an ImageData object so I can display it on a canvas?

Upvotes: 1

Views: 3023

Answers (2)

user1693593
user1693593

Reputation:

I suspect your example code is just that, an example, but just in case it isn't there are easier way to fill an area with a single color:

ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, 500, 500);

But back to flattening the array. If performance is a factor you can do it in for example the following way:

(side note: if possible - store the color information directly in the same type and byte-order you want to use as converting from string to number can be relatively costly when you deal with tens of thousands of pixels - binary/numeric storage is also cheaper).

Simply unwind/flatten the 2D array directly to a typed array:

var width = 500, height = 500;
var data32 = new Uint32Array(width * height); // create Uint32 view + underlying ArrayBuffer

for(var x, y = 0, p = 0; y < height; y++) {   // outer loop represents rows
  for(x = 0; x < width; x++) {                // inner loop represents columns
    data32[p++] = str2uint32(array[x][y]);    // p = position in the 1D typed array
  }
}

We also need to convert the string notation of the color to a number in little-endian order (format used by most consumer CPUs these days). Shift, AND and OR operations are multiple times faster than working on string parts, but if you can avoid strings at all that would be the ideal approach:

// str must be "#RRGGBB" with no alpha.
function str2uint32(str) {
  var n = ("0x" + str.substr(1))|0; // to integer number 
  return 0xff000000        |        // alpha (first byte)
         (n << 16)         |        // blue  (from last to second byte)
         (n & 0xff00)      |        // green (keep position but mask)
         (n >>> 16)                 // red   (from first to last byte)
}

Here we first convert the string to a number - we shift it right away to a Uint32 value to optimize for the compiler now knowing we intend to use the number in the following conversion as a integer number.

Since we're most likely on a little endian plaform we have to shift, mask and OR around bytes to get the resulting number in the correct byte order (i.e. 0xAABBGGRR) and OR in a alpha channel as opaque (on a big-endian platform you would simply left-shift the entire value over 8 bits and OR in an alpha channel at the end).

Then finally create an ImageData object using the underlying ArrayBuffer we just filled and give it a Uint8ClampedArray view which ImageData require (this has almost no overhead since the underlying ArrayBuffer is shared):

var idata = new ImageData(new Uint8ClampedArray(data32.buffer), width, height);

From here you can use context.putImageData(idata, x, y).

Example

Here filling with a orange color to make the conversion visible (if you get a different color than orange then you're on a big-endian platform :) ):

var width = 500, height = 500;
var data32 = new Uint32Array(width * height);
var screen = [];

// Fill with orange color
for(var x = 0; x < width; x++ ){
  screen[x] = [];
  for(var y = 0; y < height; y++ ){
    screen[x][y] = "#ff7700";  // orange to check final byte-order
  }
}

// Flatten array
for(var x, y = 0, p = 0; y < height; y++){
  for(x = 0; x < width; x++) {
    data32[p++] = str2uint32(screen[x][y]);
  }
}

function str2uint32(str) {
  var n = ("0x" + str.substr(1))|0;
  return 0xff000000 | (n << 16) | (n & 0xff00) | (n >>> 16)
}

var idata = new ImageData(new Uint8ClampedArray(data32.buffer), width, height);
c.getContext("2d").putImageData(idata, 0, 0);
<canvas id=c width=500 height=500></canvas>

Upvotes: 2

user3297291
user3297291

Reputation: 23372

Flattening arrays

The first thing you'll have to learn is how to flatten a 2d array. You can use a nested loop and push to a new 1d array, but I prefer to use reduce and concat:

const concat = (xs, ys) => xs.concat(ys);

console.log(
  [[1,2,3],[4,5,6]].reduce(concat)
)

Now you'll notice quickly enough that your matrix will be flipped. ImageData concatenates row by row, but your matrix is grouped by column (i.e. [x][y] instead of [y][x]). My advice is to flip your nested loop around :)

From "#ffffff" to [255, 255, 255, 255]

You now have the tool to create a 1d-array of hex codes (screen.reduce(concat)), but ImageData takes an Uint8ClampedArray of 0-255 values! Let's fix this:

const hexToRGBA = hexStr => [
    parseInt(hexStr.substr(1, 2), 16),
    parseInt(hexStr.substr(3, 2), 16),
    parseInt(hexStr.substr(5, 2), 16),
    255
  ];
  
console.log(
  hexToRGBA("#ffffff")
);

Notice that I skip the first "#" char and hard-code the alpha value to 255.

Converting from hex to RGBA

We'll use map to convert the newly created 1d array at once:

screen.reduce(concat).map(hexToRGBA);

2d again?!

Back to square one... We're again stuck with an array of arrays:

[ [255, 255, 255, 255], [255, 255, 255, 255], /* ... */ ]

But wait... we already know how to fix this:

const flattenedRGBAValues = screen
  .reduce(concat)  // 1d list of hex codes
  .map(hexToRGBA)  // 1d list of [R, G, B, A] byte arrays
  .reduce(concat); // 1d list of bytes

Putting the data to the canva

This is the part that was linked to in the comments, but I'll include it so you can have a working example!

const hexPixels = [
  ["#ffffff", "#000000"],
  ["#000000", "#ffffff"]
];

const concat = (xs, ys) => xs.concat(ys);

const hexToRGBA = hexStr => [
    parseInt(hexStr.substr(1, 2), 16),
    parseInt(hexStr.substr(3, 2), 16),
    parseInt(hexStr.substr(5, 2), 16),
    255
  ];

const flattenedRGBAValues = hexPixels
  .reduce(concat)  // 1d list of hex codes
  .map(hexToRGBA)  // 1d list of [R, G, B, A] byte arrays
  .reduce(concat); // 1d list of bytes

// Render on screen for demo
const cvs = document.createElement("canvas");
cvs.width = cvs.height = 2;
const ctx = cvs.getContext("2d");
const imgData = new ImageData(Uint8ClampedArray.from(flattenedRGBAValues), 2, 2);
ctx.putImageData(imgData, 0, 0);

document.body.appendChild(cvs);
canvas { width: 128px; height: 128px; image-rendering: pixelated; }

Upvotes: 8

Related Questions