Chapter 89

砂遊びシミュレーション

miku
miku
2021.11.19に更新
このチャプターの目次

実行結果

ピクセル単位の砂が上から落ちていき、マウスで画面をクリックすると壁を作ることができる。

アルゴリズム

const fromX = floor(canvas.width / 3);
const toX = floor((canvas.width / 3) * 2);
for (let x = fromX; x <= toX; x++) {
  if (random() < 0.1) {
    setPixel(x, 0, Color.Sand);
  }
}

毎フレーム一定確率で画面上部中央に砂のピクセルを書き込む。

for (let y = canvas.height - 1; y >= 0; y--) {
  for (let x = 0; x < canvas.width; x++) {
    fall(x, y);
  }
}

画面の下から上に向けてピクセルを走査する。

function fall(x, y) {
  const cur = getPixel(x, y);
  if (!colorCompare(cur, Color.Sand)) {
    return;
  }
  if (canvas.height - 1 <= y) {
    setPixel(x, y, Color.None);
  } else if (colorCompare(getPixel(x, y + 1), Color.None)) {
    setPixel(x, y + 1, cur);
    setPixel(x, y, Color.None);
  } else {
    const tx = x + floor(random(-4, 4));
    if (colorCompare(getPixel(tx, y), Color.None)) {
      setPixel(x, y, Color.None);
      setPixel(tx, y, Color.Sand);
    }
  }
}

fall() に指定した座標が砂の場合のみ更新の対象になる。最下段のピクセルなら砂のピクセルを空にする。そうでない場合、ひとつ下のピクセルが空なら砂を下に移動する。更にそうでない場合、ランダムな左右数ピクセルが空ならそちらに砂を移動する。

コード例

const Color = {
  None: [0x1a, 0x1a, 0x1a, 255],
  Wall: [0x4e, 0x4a, 0x4e, 0xff],
  Sand: [0xd2, 0x7d, 0x2c, 0xff],
};
let canvas;

function setup() {
  createCanvas(windowWidth, windowHeight);

  noSmooth();
  canvas = createImage(500, 500);
  fillRect(0, 0, canvas.width, canvas.height, Color.None);
}

function mouseDragged() {
  fillRect(mouseX, mouseY, 10, 10, Color.Wall);
}

function draw() {
  canvas.loadPixels();

  const fromX = floor(canvas.width / 3);
  const toX = floor((canvas.width / 3) * 2);
  for (let x = fromX; x <= toX; x++) {
    if (random() < 0.1) {
      setPixel(x, 0, Color.Sand);
    }
  }

  for (let y = canvas.height - 1; y >= 0; y--) {
    for (let x = 0; x < canvas.width; x++) {
      fall(x, y);
    }
  }
  canvas.updatePixels();
  image(canvas, 0, 0, canvas.width, canvas.height);
}

function fillRect(x, y, width, height, color) {
  canvas.loadPixels();
  for (let ty = y; ty < y + height; ty++) {
    for (let tx = x; tx < x + width; tx++) {
      setPixel(tx, ty, color);
    }
  }
  canvas.updatePixels();
  image(canvas, 0, 0, canvas.width, canvas.height);
}

function fall(x, y) {
  const cur = getPixel(x, y);
  if (!colorCompare(cur, Color.Sand)) {
    return;
  }
  if (canvas.height - 1 <= y) {
    setPixel(x, y, Color.None);
  } else if (colorCompare(getPixel(x, y + 1), Color.None)) {
    setPixel(x, y + 1, cur);
    setPixel(x, y, Color.None);
  } else {
    const tx = x + floor(random(-4, 4));
    if (colorCompare(getPixel(tx, y), Color.None)) {
      setPixel(x, y, Color.None);
      setPixel(tx, y, Color.Sand);
    }
  }
}

function colorCompare(a, b) {
  return a[0] == b[0] && a[1] == b[1] && a[2] == b[2];
}

function getPixel(x, y) {
  const i = (y * canvas.width + x) * 4;
  return [canvas.pixels[i], canvas.pixels[i + 1], canvas.pixels[i + 2], canvas.pixels[i + 3]];
}

function setPixel(x, y, color) {
  const i = (y * canvas.width + x) * 4;
  canvas.pixels[i + 0] = color[0];
  canvas.pixels[i + 1] = color[1];
  canvas.pixels[i + 2] = color[2];
  canvas.pixels[i + 3] = color[3];
}