创建具有不同颜色比例的双色 canvas

creating a bi-colored canvas with varying color proportions

我想使用 javascript 创建一个包含两种颜色(例如橙色和蓝色)的 128x128 像素的图像。像素应随机排列,我希望能够改变两种颜色的比例(例如,状态为“0.4 橙色”以获得 40% 橙色和 60% 蓝色像素)。

它应该看起来像这样:

我在这里找到了一个脚本来创建一个 canvas 具有随机像素颜色并修改它只给我橙色的。我基本上正在努力的是创建 "if statement" 将颜色值分配给像素。我想创建一个数组,其中 propotion_orange*128*128 个唯一元素在 1 到 128*128 之间随机绘制。然后为每个像素值检查它是否在该数组中,如果是,则将橙色分配给它,否则为蓝色。作为 JS 的新手,我在创建这样的数组时遇到了麻烦。我希望我能够以一种可以理解的方式陈述我的问题...

这是我找到的用于随机像素颜色的脚本,我修改后只给我橙色:

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);

您可以为 canvas 上的像素数创建一个颜色数组。对于每个像素,您可以指定一个 rgba 数组,其中包含该像素应采用的 rgba 值。最初,您可以使用 40% 橙色 rgba 数组和 60% 蓝色 rgba 数组填充颜色数组。然后,您可以打乱这个数组以获得颜色的随机分布。最后,您可以遍历每个像素并使用颜色数组中的关联值设置它的颜色。

参见下面的示例:

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: 
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();

如果你想要一个粗略的分配,你可以简单地在 if 条件中使用 Math.random() 像这样:

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

这为您提供了 canvas 中橙色像素的平均 {percentage} 数量。每次创建 canvas 时,确切的数量都不同。 canvas 越大,您就越接近实际百分比,正如您对大数字的概率所期望的那样。

如果你想要每次都精确分配有点乏味:

  1. 创建一个大小为 #"pixelAmount"
  2. 的数组
  3. 用 1 和 0 的确切数量填充数组
  4. 随机打乱数组(我用的是fisherYates算法)

可能还有其他方法可以实现此目的,但我发现这相当简单。

我添加了一些奇思妙想来玩转百分比 ;-)

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>

只是为了好玩,这是一个可能尽可能高效的版本(使用不精确的概率),尽管通过使用 32 位 ABGR 常量可能会以可移植性为代价:

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);

在 OP 的 128x128 分辨率下,这可以简单地每帧刷新整个 canvas 一次,并留下 CPU 的负载。在 https://jsfiddle.net/alnitak/kbL9y0j5/18/

进行演示

洗牌到位

一些答案建议使用 Fisher Yates 算法来创建像素的随机分布,这是迄今为止获得一组固定值(在本例中为像素)的随机均匀分布的最佳方法

然而,这两个答案都创建了非常糟糕的实现,复制像素(使用比需要更多的 RAM,因此需要额外的 CPU 周期)并且都处理每个通道的像素而不是作为谨慎的项目(咀嚼更多 CPU 周期)

使用您从ctx.getImageData 获得的图像数据来保存要随机播放的数组。它还提供了一种将 CSS 颜色值转换为像素数据的便捷方法。

随机播放现有图片

以下随机播放函数将混合任何 canvas 并保留所有颜色。使用 32 位类型数组,您可以在一次操作中移动一个完整的像素。

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
}

演示

此演示添加了一些功能。函数 fillRatioAndShffle 为每种颜色绘制 1 个像素到 canvas,然后使用像素数据作为 Uint32 来设置颜色比率,并使用标准混洗算法 (Fisher Yates)

使用滑块更改颜色比例。

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

三元?而不是if

没有人会注意到混音是多了还是少了。更好的方法是按赔率混合(如果随机值低于固定赔率)。

对于两个值,三元是最优雅的解决方案

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

请参阅 Alnitak 或下一个代码段中相同方法的替代演示变体。

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