Warm tip: This article is reproduced from stackoverflow.com, please click
arrays canvas javascript pixel

creating a bi-colored canvas with varying color proportions

发布于 2020-04-03 23:49:24

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);
Questioner
David
Viewed
60
Blindman67 2020-02-01 19:18

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" />