2

I would like to create an image that consists of 128x128 pixles in two colors (e.g. orange and blue) using javascript. The pixels should be arranged randomly and I would like to be able to vary the proportion of the two colors (e.g. state sth like "0.4 orange" to get a 40% orange and 60% blue pixels).

It should look somewhat like this:

it should look somewhat like this

I have found a script here to create a canvas with random pixels colors and modified it to give me only orange ones. What I am basically struggeling with is to create the "if statement" that assigns the color values to the pixels. I though about creating an array with propotion_orange*128*128 unique elements randomly drawn between 1 and 128*128. and then for each pixel value check if its in that array and if yes assign orange to it else blue. Being completely new to JS i am having troubles creating such an array. I hope i was able to state my problem in an understandable fashion...

that's the script i had found for random pixel colors that i modified to give me only orange:

var canvas = document.createElement('canvas');
canvas.width = canvas.height = 128;
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);

for (var i = 0; i < imgData.data.length; i += 4) {
  imgData.data[i] = 255; // red
  imgData.data[i + 1] = 128; // green
  imgData.data[i + 2] = 0; // blue
  imgData.data[i + 3] = 255; // alpha
}

ctx.putImageData(imgData, 0, 0);
document.body.appendChild(canvas);
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
David
  • 23
  • 3

4 Answers4

3

Shuffle in place

Some answers suggest using the Fisher Yates algorithm to create the random distribution of pixels, which is by far the best way to get a random and even distribution of a fixed set of values (pixels in this case)

However both answers have created very poor implementations that duplicate pixels (Using more RAM than needed and thus extra CPU cycles) and both handle pixels per channel rather than as discreet items (chewing more CPU cycles)

Use the image data you get from ctx.getImageData to hold the array to shuffle. it also provides a convenient way to convert from a CSS color value to pixel data.

Shuffle existing image

The following shuffle function will mix any canvas keeping all colors. Using a 32bit typed array you can move a complete pixel in one operation.

function shuffleCanvas(ctx) {
    const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    const p32 = new Uint32Array(imgData.data.buffer); // get 32 bit pixel data
    var i = p32.length, idx;
    while (i--) {
        const b = p32[i];
        p32[i] = p32[idx = Math.random() * (i + 1) | 0];
        p32[idx] = b;
    }
    ctx.putImageData(imgData, 0, 0); // put shuffled pixels back to canvas
}

Demo

This demo adds some functionality. The function fillRatioAndShffle draws 1 pixel to the canvas for each color and then uses the pixel data as Uint32 to set the color ratio and the shuffles the pixel array using a standard shuffle algorithm (Fisher Yates)

Use slider to change color ratio.

const ctx = canvas.getContext("2d");
var currentRatio;
fillRatioAndShffle(ctx, "black", "red", 0.4);

ratioSlide.addEventListener("input", () => {
    const ratio = ratioSlide.value / 100;
    if (ratio !== currentRatio) { fillRatioAndShffle(ctx, "black", "red", ratio) }
});    

function fillRatioAndShffle(ctx, colA, colB, ratio) {
    currentRatio = ratio;
    ctx.fillStyle = colA;
    ctx.fillRect(0, 0, 1, 1);
    ctx.fillStyle = colB;
    ctx.fillRect(1, 0, 1, 1);

    const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    const p32 = new Uint32Array(imgData.data.buffer); // get 32 bit pixel data
    const pA = p32[0], pB = p32[1]; // get colors 
    const pixelsA = p32.length * ratio | 0;
    p32.fill(pA, 0, pixelsA);
    p32.fill(pB, pixelsA);
    !(ratio === 0 || ratio === 1) && shuffle(p32);  
    ctx.putImageData(imgData, 0, 0); 
}

function shuffle(p32) {
    var i = p32.length, idx, t;
    while (i--) {
        t = p32[i];
        p32[i] = p32[idx = Math.random() * (i + 1) | 0];
        p32[idx] = t;
    }
}
<canvas id="canvas" width="128" height="128"></canvas>
<input id="ratioSlide" type="range" min="0" max="100" value="40" />

Ternary ? rather than if

No human is ever going to notice if the mix is a little over or a little under. The much better method is to mix by odds (if random value is below a fixed odds).

For two values a ternary is the most elegant solution

pixel32[i] = Math.random() < 0.4 ? 0xFF0000FF : 0xFFFF0000; 

See Alnitak excellent answer or an alternative demo variant of the same approach in the next snippet.

const ctx = canvas.getContext("2d");
var currentRatio;
const cA = 0xFF000000, cB = 0xFF00FFFF; // black and yellow
fillRatioAndShffle(ctx, cA, cB, 0.4);

ratioSlide.addEventListener("input", () => {
    const ratio = ratioSlide.value / 100;
    if (ratio !== currentRatio) { fillRatioAndShffle(ctx, cA, cB, ratio) }
    currentRatio = ratio;
});    

function fillRatioAndShffle(ctx, cA, cB, ratio) {
    const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
    const p32 = new Uint32Array(imgData.data.buffer); // get 32 bit pixel data
    var i = p32.length;
    while (i--) { p32[i] = Math.random() < ratio ? cA : cB }
    ctx.putImageData(imgData, 0, 0); 
}
<canvas id="canvas" width="128" height="128"></canvas>
<input id="ratioSlide" type="range" min="0" max="100" value="40" />
Blindman67
  • 51,134
  • 11
  • 73
  • 136
2

You can create a color array for the number of pixels on your canvas. For each pixel, you can specify the a rgba array which holds the rgba values that that pixel should take. Initially, you can populate the colors array with 40% orange rgba arrays and 60% blue rgba arrays. Then, you can shuffle this array to get a random distribution of colours. Lastly, you can loop over each pixel and set the color of it using it's associated value from the color array.

See example below:

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 128;
const ctx = canvas.getContext('2d');
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
document.body.appendChild(canvas);

// From: https://stackoverflow.com/a/6274381/5648954
function shuffle(a) {
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function generateColors() {
  const randomColorsArr = [];
  const colors = {
    orange: [255, 128, 0, 255],
    blue: [0, 0, 255, 255]
  }
  // For each pixel add an rgba array to randomColorsArr
  for (let i = 0; i < imgData.data.length/4; i++) {
    if (i/(imgData.data.length/4) < 0.4)
      randomColorsArr.push(colors.orange); // add orange to fill up the first 40% of the array
    else
      randomColorsArr.push(colors.blue); // add blue to fill up the remaining 60% of the array
  }
  return shuffle(randomColorsArr); // shuffle the colors around
}

// For each pixel, get and set the color
function displayPx() {
  const randomColorsArr = generateColors();
  for (let i = 0; i < randomColorsArr.length; i++) {
    let rgba = randomColorsArr[i];
    for (let j = 0; j < rgba.length; j++)
      imgData.data[i*4 + j] = rgba[j];
  }
  ctx.putImageData(imgData, 0, 0);
}

displayPx();
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
  • Depending on what the OP needs this might be on point or missing it completely. `Math.random()` will give you an approximate distribution but not exact (obviously). – Christoph Jan 31 '20 at 13:19
  • @Christoph yeah, I somewhat misread the question. I should probably change it so it gives exact. – Nick Parsons Jan 31 '20 at 13:19
  • You are creating a new array for each pixel! Thats at least 40 bytes per pixel, plus all the extra CPU cycles. You should have defined the two arrays once and pushed references to the array. – Blindman67 Jan 31 '20 at 19:02
  • @Blindman67 thanks. I wasn't thinking much about memory usage but really should have. It should be using references now. I like your approach though, it's the proper/better way to do it. – Nick Parsons Feb 01 '20 at 00:42
  • 1
    @all thanks for your solutions! and your mutual feedback. given my current level in JS Nicks's answer was the easiest to understand. yet I figured that in terms of efficiency Blindman's/Alnitaks seem to be the best (although not all that straight forward to me ;) – David Feb 04 '20 at 13:29
2

If you want a rough distribution, you can simply use Math.random() in the if condition like this:

if (Math.random() <= percentage){
  setPixel(color1);
} else {
  setPixel(color2);
}

This gives you an average amount of {percentage} of orange pixels in the canvas. The exact amount differs every time you create the canvas. The bigger the canvas the closer you are to the actual percentage as you would expect with probability with big numbers.

If you want a precise distribution every time it get's a bit tedious:

  1. create an Array of size #"pixelAmount"
  2. fill the array with the exact amount of 1 and 0
  3. randomly shuffle the Array (I used the fisherYates algorithm)

There are probably other ways to achieve this, but I found this rather straightforward.

I added some fanciness to play around with the percentages ;-)

var canvas = document.createElement('canvas');
canvas.width = canvas.height = 128;
var ctx = canvas.getContext('2d');
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let pixelAmount = canvas.width * canvas.height;
let colorArray = new Array(pixelAmount - 1);

// The values you want
let percentage;
let color1 = [255, 128, 0, 255];
let color2 = [0, 0, 255, 255];

document.body.appendChild(canvas);


function colorize() {
  // this just checks the amount of pixels colored with color1
  let testColor1 = 0;
  for (var i = 0; i < imgData.data.length; i += 4) {
    if (colorArray[i/4]) {
      setPixelColor(i, color1);
      testColor1++;
    } else {
      setPixelColor(i, color2);
    }
  }
  console.clear();
  console.log(`pixels:${pixelAmount} / color1:${testColor1} / percentage:${Math.round(testColor1/pixelAmount*100)}%`);
  ctx.putImageData(imgData, 0, 0);
}


function setPixelColor(pixel, color) {
  imgData.data[pixel] = color[0]; // red
  imgData.data[pixel + 1] = color[1]; // green
  imgData.data[pixel + 2] = color[2]; // blue
  imgData.data[pixel + 3] = color[3]; // alpha
}




function fisherYates(array) {
  var i = array.length;
  if (i == 0) return false;
  while (--i) {
    var j = Math.floor(Math.random() * (i + 1));
    var tempi = array[i];
    var tempj = array[j];
    array[i] = tempj;
    array[j] = tempi;
  }
}

let input = document.querySelector("input");
input.addEventListener("change", recalculate);
document.querySelector("button").addEventListener("click",recalculate);

function recalculate() {
  percentage = input.value;
  console.log("triggered:"+percentage);
  colorPixels = Math.round(pixelAmount * percentage);
  colorArray.fill(1, 0, colorPixels);
  colorArray.fill(0, colorPixels + 1);
  fisherYates(colorArray);
  colorize();
}
recalculate();
input {
  width: 50px;
}
<input type="number" min="0" max="1" step="0.05" value="0.05"> 
<button>new</button>
<br>
Christoph
  • 50,121
  • 21
  • 99
  • 128
2

Just for fun, here's a version (using inexact probability) that's probably about as efficient as it's possible to get, albeit at the possible expense of portability through its use of 32-bit ABGR constants:

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 128;
const ctx = c.getContext('2d');
const imgData = ctx.createImageData(c.width, c.height);
const data = imgData.data;
const buf = data.buffer;
const u32 = new Uint32Array(buf);

for (let i = 0; i < u32.length; ++i) {
    u32[i] = Math.random() < 0.4 ? 0xff0080ff : 0xffff0000;
}    
ctx.putImageData(imgData, 0, 0);

At the OP's 128x128 resolution this can trivially refresh the entire canvas once per frame with loads of CPU left over. Demo at https://jsfiddle.net/alnitak/kbL9y0j5/18/

Alnitak
  • 334,560
  • 70
  • 407
  • 495