создание двухцветного холста с различными цветовыми пропорциями - PullRequest
2 голосов
/ 31 января 2020

Я хотел бы создать изображение, состоящее из 128x128 пикселей в двух цветах (например, оранжевый и синий), используя javascript. Пиксели должны располагаться случайным образом, и я хотел бы иметь возможность изменять пропорции двух цветов (например, указывать sth, например, «0,4 оранжевого», чтобы получить 40% оранжевого и 60% синего пикселей).

Это должно выглядеть примерно так:

it should look somewhat like this

Я нашел здесь скрипт для создания холста со случайными цветами пикселей и изменил его так, чтобы он отображал только оранжевые цвета. То, с чем я в основном борюсь - это создание оператора if, который присваивает значения цвета пикселям. Я думал о создании массива с уникальными элементами 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);

Ответы [ 4 ]

3 голосов
/ 31 января 2020

Перемешать на месте

В некоторых ответах предлагается использовать алгоритм Фишера Йейтса для создания случайного распределения пикселей, что является наилучшим способом получения случайного и равномерного распределения фиксированного набора значений (пикселей в этом случае)

Однако оба ответа создали очень плохие реализации, которые дублируют пиксели (используя больше оперативной памяти, чем необходимо, и, следовательно, дополнительные циклы ЦП), и оба обрабатывают пиксели на канал, а не как дискретные элементы (пережевывая больше циклов ЦП)

Используйте данные изображения, полученные из ctx.getImageData, чтобы удерживать массив для перемешивания. он также предоставляет удобный способ преобразования значения цвета CSS в пиксельные данные.

Перемешать существующее изображение

Следующая функция перемешивания будет смешивать любой холст, сохраняя все цвета. Используя 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 dr aws 1 пиксель на холсте для каждого цвета, а затем использует данные пикселей в качестве Uint32 для установки цветового соотношения и перетасовывает массив пикселей, используя стандартный алгоритм перемешивания (Фишер Йейтс)

Используйте ползунок для изменения цветового соотношения.

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; 

См. Альнитак отличный ответ или альтернативный демонстрационный вариант того же подхода в следующем фрагменте.

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" />
2 голосов
/ 31 января 2020

Если вы хотите черновое распределение, вы можете просто использовать Math.random() в условии if, подобном этому:

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

Это дает вам среднее количество {процентов} оранжевых пикселей на холсте. Точная сумма отличается каждый раз, когда вы создаете холст. Чем больше холст, тем ближе вы к фактическому проценту, как и следовало бы ожидать с большой вероятностью.

Если вам нужно точное распределение каждый раз, когда оно становится немного утомительным:

  1. создать массив размером # "pixelAmount"
  2. заполнить массив точным количеством 1 и 0
  3. случайным образом перемешать массив (я использовал алгоритм 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>
2 голосов
/ 31 января 2020

Просто для забавы, вот версия (использующая неточную вероятность), которая, вероятно, настолько эффективна, насколько это возможно, хотя и за счет возможного переноса благодаря использованию 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 это может легко переосмыслить sh всего холста один раз за кадр с нагрузкой на процессор. Демо на https://jsfiddle.net/alnitak/kbL9y0j5/18/

2 голосов
/ 31 января 2020

Вы можете создать цветовой массив для количества пикселей на холсте. Для каждого пикселя вы можете указать массив rgba, который содержит значения rgba, которые должен принимать этот пиксель. Изначально вы можете заполнить массив цветов 40% -ыми оранжевыми массивами rgba и 60% -ыми голубыми массивами rgba. Затем вы можете перемешать этот массив, чтобы получить случайное распределение цветов. Наконец, вы можете l oop на каждый пиксель и установить его цвет, используя соответствующее значение из цветового массива.

См. Пример ниже:

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