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:
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);
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.
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
}
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" />
?
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" />