Chapter 12

並べる

miku
miku
2021.11.23に更新

複数のオブジェクトを描画する場合、位置が被らないようにしたり、比較したりするために等間隔に並べることがある。この章ではその並べ方について学ぶ。

等間隔に並べる

等間隔に並べるということは一定のスペースを空けて配置していくということだ。まずは簡単に、横に一定のスペースを空けて配置していく方法を考える。

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

  const space = 60;
  const n = 20;

  for (let x = 0; x < n; x++) {
    const tx = x * space;
    const ty = height / 2;

    circle(tx, ty, 30);
  }
}

space が間隔、n が円を並べる個数だ。y座標を固定して、x座標を space * 0, space * 1, space * 2 ...と変化させ円を並べていく。

これで考えられる問題は、n の値を適当に決めているので、画面サイズの可変に適していないことだろう。並べる以上、数が足りなくて隙間が空いていると不格好な形になるが、それを防ぐには、かなり多めに n を取らなければならない。

次は画面サイズを意識したコードを考える。

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

  const space = 60;
  const n = 10000;

  for (let x = 0; x < n; x++) {
    const tx = x * space;
    const ty = height / 2;

    if (tx > width) {
      break;
    }

    circle(tx, ty, 30);
  }
}

まず n の値をどう考えてもこれだけあれば十分だろうというぐらい大きい値を入れておく。そして円の位置 tx が画面幅を超えてしまったらループを抜ければいい。

実はもう少し微調整が必要な部分がある。それは右端の境界処理だ。

縦の白線の位置が右の画面端だと思ってもらいたい。上の並びが先程の作例だが、下の並びはループの終了条件を少し右に伸ばした形になる。

上の並びの右側が妙に隙間が空いているのは、円のx座標が画面幅を超えたら終了というコードにしているからだ。円の座標は円の中央部分なので、円の座標が画面から切れていても、円の左側半分が画面に表示される可能性がある。実際下の並びがそうだ。左側の円が切れて見えている以上、下の並びのほうが自然に見えるはずだ。

下側のような表示になるように調整してみよう。2つほど思いついた方法を紹介する。

if (tx - r >= width) {
  break;
}

1つ目の方法は、円の中央ではなく、円の左側が画面から切れたらループを抜けるということである。tx が円の中央なので、そこから半径を引けば左側の座標が取得できる。

if (tx >= width + space) {
  break;
}

2つ目の方法は、終了位置を space 分だけ右にずらすということだ。つまりループを一回分だけ増やせばいい。

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

  const space = 60;
  const n = 10000;
  const r = 15;

  for (let x = 0; x < n; x++) {
    const tx = x * space;
    const ty = height / 2;

    if (tx - r >= width) {
      break;
    }

    circle(tx, ty, r * 2);
  }
}

これで格好がついたので、縦にも円を配置していくことにする。

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

  const space = 60;
  const n = 10000;
  const r = 15;

  for (let y = 0; y < n; y++) {
    const ty = y * space;

    if (ty - r >= height) {
      break;
    }

    for (let x = 0; x < n; x++) {
      const tx = x * space;

      if (tx - r >= width) {
        break;
      }

      circle(tx, ty, r * 2);
    }
  }
}

数を固定して等間隔に並べる

縦横に並べる円の数を前もって決めておき、その円の数によって空けるスペースを決めて、等間隔に並べるという方法がある。先ほどと同じように、まずは横に並べていく方法について考える。

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

  const n = 10;
  for (let x = 0; x < n; x++) {
    const tx = (width / n) * x;
    const ty = height / 2;

    circle(tx, ty, 30);
  }
}

画面幅を円の数で割ることで円を置く間隔 space を計算する。y座標を固定して、x座標を space * 0, space * 1, space * 2 ...と横に円を配置していく。

しかしよく見てみると右側に隙間が空いている。本来その隙間に置かれるであろう円のx座標は (width / n) * n なのに、ループの終了条件が x < n なので、xn になることがないのが原因だ。

for文のループを1-index、つまり for (let i = 1; i <= n; i++) {} にすると右側の隙間はなくなるが、代わりに左側に隙間が出てきてしまう。

これは解決するために、理想的な配置を確認しておこう。上のラインが円を2個、下のラインが円を3個用意した場合の理想的な配置になる。

円が2個の場合は隙間のサイズは画面幅に、3個の場合は隙間のサイズが画面幅の半分になっているように見える。なので、一般化すると、円が n 個の場合は隙間のサイズを width / (n - 1) にすればよさそうだ。

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

  const n = 10;
  const space = width / (n - 1);
  for (let x = 0; x < n; x++) {
    const tx = x * space;
    const ty = height / 2;

    circle(tx, ty, 30);
  }
}

画面幅を円の数で割るのではなく、代わりに 円の数-1、つまり隙間の数で割ったものを space に入れる。

これで問題が解決したので、縦にも円を配置していくことにする。

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

  const n = 10;
  for (let y = 0; y < n; y++) {
    for (let x = 0; x < n; x++) {
      const tx = (width / (n - 1)) * x;
      const ty = (height / (n - 1)) * y;

      circle(tx, ty, 30);
    }
  }
}

等間隔に配置することができたが実はまだ問題が残っている。画面サイズが小さくなると、円の配置間隔が小さくなり、円同士の描画が被ってしまうことだ。

この場合は円の半径を画面サイズに合わせて可変するか、画面サイズに合わせて円の数を減らすなどの対処方法が考えられる。

画面内に配置する

const n = 5;
const w = width / n;
const h = height / n;

縦、横ともに分割する数を n だとすると、分割した1つあたりの幅は width / n, 縦幅は height / n になる。この領域内で中央に円を配置すれば、結果的に画面全体で円が等間隔に揃うことになる。

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

  const n = 5;
  const w = width / n;
  const h = height / n;
  for (let y = 0; y < n; y++) {
    for (let x = 0; x < n; x++) {
      drawCircle(w * x, h * y, w, h);
    }
  }
}

function drawCircle(x, y, w, h) {
  translate(x, y);
  circle(w / 2, h / 2, 20);
  resetMatrix();
}

drawCircle(x, y, w, h) では (x, y)~(x + w, y + h) の矩形領域を全体の画面サイズだと考えて、その中央に円を描画する。ただし、左上座標が (0, 0) ではないので、この領域の中央座標は (w / 2, h / 2) ではなく、(x + w / 2, y + h / 2) になる。この指定が面倒な場合は translate(x, y) で座標の原点を (x, y) に移動しておけば、中央座標は (w / 2, h / 2) と指定できる。

どこまで円のサイズの増やせるのか

各矩形領域の幅と高さが分かっているので、wh 小さい方のサイズを円の直径にすると円同士が接触するかしないかのギリギリのサイズになるので、その直径未満にすれば円同士が被ることはない。

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

  const n = 5;
  for (let y = 0; y < n; y++) {
    for (let x = 0; x < n; x++) {
      drawCircle((width / n) * x, (height / n) * y, width / n, height / n);
    }
  }
}

function drawCircle(x, y, w, h) {
  translate(x, y);
  circle(w / 2, h / 2, min(w, h) - 5);
  resetMatrix();
}

min(w, h) が先程説明したギリギリのサイズになるので、そこから幾つかの値を引けばいい。