Solution 1 :

At the moment, you are doing the following for every pixel

  • Create canvas
  • get the context and draw to the image
  • get the context and get the pixel data for one pixel
  • create a DIV
  • add it to the DOM

Now, lets streamline this

The following can be done ONCE

  • create the canvas
  • get the context
  • use the context to draw the image
  • use the context to get the image data
  • create an empty string

Now, for each pixel

  • get the pixel data
  • add the html for the div to the string

And finally, just ONCE

  • Add the string with all the divs to the DOM

Something like:

const componentToHex = c => c.toString(16).padStart(2, '0');
const rgbToHex = (r, g, b) => `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
const img = document.getElementById('my-image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const w = canvas.width = img.width;
const h = canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
const imageData = context.getImageData(0, 0, w, h).data;
const pixel = (x, y) => {
    const index = (w * y + x) * 4;
    return rgbToHex(imageData[index], imageData[index + 1], imageData[index + 2]);
}
const df = document.createDocumentFragment();
for (let y = 0; y < img.height; y++) {
    for (let x = 0; x < img.width; x++) {
        const div = df.appendChild(document.createElement('div'));
        div.style.top = y + "px";
        div.style.left = x + "px";
        div.style.backgroundColor = pixel(x, y);
    }
}
document.body.appendChild(df);

Note: it may not be the case now, but such a loop may work faster inside a function – often loops in the global context are slower

So, you could wrap the whole code above in

(() => {
    // the code from above
})();

And see significant improvement again – not sure, used to be the case in years gone by

changed to use document fragment for a further 25% speed improvement
Now takes 1.4 seconds in firefox for a 640×480 image, 2.3 seconds in chrome – which didn’t really see a big difference between using insertAdjacentHTML vs a document fragment

another thing to note. In Firefox the page becomes sluggish, in chrome, for 640×480, no such issue

Problem :

Before you go off saying I’m crazy, believe me, I know. I’m not going for a website that renders fast, loads fast, or gets a high lighthouse score. I just want it to work.

I have some javascript that picks up all the pixel colors of an image. With this function, I create a div element that is 1px by 1px and set the background color to the pixel color of those same coordinates. Then the coordinates are used to set the top and left values. My code does what it’s told.

Here’s my problem, my image is 700px by 387px. If you do the math, that works out to 270,900 html elements. Chrome, simply isn’t built for this madness. I want to see this work, I want to “manually” create an image with div elements, somehow. My cpu maxes out when I try to do so, and I’m sure I’d run out of ram eventually.

Everything works fine if I only try hundreds or a few thousand pixels, but any more, and chrome dies. I’m not sure if it’s calculating in the browser that may be my problem, or if chrome cant display this many elements, or both. I suppose I could do the same math on my server with python, and append it to html, but then chrome probably couldn’t display it.

Obviously, this isn’t super important, just fun. I think the community will enjoy the challenge as well.

Here’s calculating 100 pixels:

onload = e => {

  function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
  }

  function rgbToHex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
  }

  function img(x, y) {
    var img = document.getElementById('my-image');
    var canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);

    var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
    return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
  }
  //x = 700 y = 387
  for (var x = 0; x < 10; x++) {
    for (var y = 0; y < 10; y++) {
      document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
    }
  }
};
div {
  position: absolute;
  width: 1px;
  height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">

here’s calculating 2500px (still works, takes a while)

onload = e => {
  function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
  }

  function rgbToHex(r, g, b) {
    return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
  }

  function img(x, y) {
    var img = document.getElementById('my-image');
    var canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);

    var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
    return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
  }
  //x = 700 y = 387
  for (var x = 0; x < 50; x++) {
    for (var y = 0; y < 50; y++) {
      document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
    }
  }
};
div {
  position: absolute;
  width: 1px;
  height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">

Cheers, Isaac.

Comments

Comment posted by ilsloaoycd

Cross orgin policies in most browsers will prevent the image from being sampled in the code snippets. You can disable them or try a different image. The data url for said images were too long but I really wanted to keep them to show the scale of the problem.

Comment posted by JLRishe

You haven’t explained why you’re trying to do this. Is this just an experiment in using DIVs to display the individual pixels of an image, or is it perhaps an XY problem?

Comment posted by Jaromanda X

erm, you’re creating a new canvas and drawing the full image for every pixel? That’s wasteful – don’t do that

Comment posted by Onur Gelmez

You can use web workers for reduce the time

Comment posted by ilsloaoycd

@JaromandaX haha yeah it was late. Definitely wasn’t the smartest choice on my part.

Comment posted by Kaiido

That’s a very good point, unfortunately, even with this huge improvement, I fear there will still be only a few devices able to render that many elements in one go. (Currently waiting for my FF to unfreeze…)

Comment posted by Here is a small test fiddle

Here is a small test fiddle

Comment posted by Jaromanda X

using insertAdjacentHTML is slower than using document fragment … 1.8 as opposed to 1.4 seconds

Comment posted by jsfiddle.net/c8ownh1y

@Kaiido – using insertAdjacentHTML will probably do that, in fact even doing document fragment line by line is just as slow (if not slower, oddly enough) – doing it in one chunk seems to be fine –

Comment posted by Jaromanda X

by the way, the code always runs in about 1.2 to 1.4 seconds on my machine … but sometimes the image doesn’t display until about a second later … guess the rendering engine is working overtime :p

By

Leave a Reply

Your email address will not be published. Required fields are marked *