ドットを重ねるときの色

前回、「2Dグラフィックスのしくみ」という本を読んでドットで線を描いてみました。

今回は四角形を2つ重ねてみます!という日記です。

結果はこうなります。

See the Pen DotRect by kariyayo (@kariyayo) on CodePen.


前回同様にCanvasにドットを打っていきます。10pixel * 10pixelの大きさを1ドットとします。

<body>
  <canvas id="canvas" width="400" height="400"></canvas>
</body>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

/**
 * @param {int} x
 * @param {int} y
 * @param {object} color - {alpha: float, red: int, green: int, blue: int}
 */
function setDot(context, x, y, color) {
  context.fillStyle = `rgb(${color.red},${color.green},${color.blue})`;
  context.globalAlpha = color.alpha; // 透明度
  context.fillRect(x*10, y*10, 10, 10); // 10ピクセル*10ピクセルの大きさの四角形をドットとみなす
}

四角形をドットで描く

これは簡単。左上と右下の2点と色を指定して四角形を描く drawRect() 関数を用意する。

左上からスタートして、まずは一番上の行をドットで描く。1行分ドットを打ったら、次の行(yの値を+1)をドットで描く。

function drawRect(context, x1, y1, x2, y2, color) {
  const xStart = Math.min(x1, x2);
  const yStart = Math.min(y1, y2);
  const xEnd = Math.max(x1, x2);
  const yEnd = Math.max(y1, y2);
  for (let y = yStart; y <= yEnd; y++) {
    for (let x = xStart; x <= xEnd; x++) {
      dot(context, x, y, color);
    }
  }
}

// 左上座標(5,5)、右下座標(20,20)の赤い四角形
drawRect(context, 5, 5, 20, 20, {alpha:1, red:255, green:0, blue:0});

gyazo.com

2つ描いてみるとどうなるか?後で描いた青い四角形が手前に表示され、赤い四角形は一部が隠れる。

// 左上座標(5,5)、右下座標(20,20)の赤い四角形
drawRect(context, 5, 5, 20, 20, {alpha:1, red:255, green:0, blue:0});

// 左上座標(10,10)、右下座標(30,30)の青い四角形
drawRect(context, 10, 10, 30, 30, {alpha:1, red:0, green:0, blue:255});

gyazo.com

アルファブレンディング

赤い四角形が隠れていたけれど、重なっている部分を透明度を考慮して色を調整してみる。透明度を考慮して合成する処理をアルファブレンディングと呼ぶ。

これまで color の表現として {alpha:1, red:255, green:0, blue:0} というオブジェクトを使ってきた。このalphaは不透明度で0~1の値をとり、0に近づくほど透明になる。0だと完全に透明。この alpha をちゃんと取り扱っていく。

上に重なる青い四角形の alpha が0.7だとどうなるか?重なる部分は下にある赤い四角形の色と、上に重ねる青い四角形の色を良い感じに混ぜた色にする必要がある。つまり、青い四角形のドットを打つときに下にあるドットの色を取得して、重ねるドットの色を決める必要がある。そのために処理を見直す。

Bufferを導入する

これまでは以下のようにdrawRect()関数からdot()関数を呼び出してCanvasのメソッドを呼んでドットを打っていた。

gyazo.com

dot()関数からCanvasのメソッドを呼ぶのをやめて、一度2次元配列をドットのBuffuerとして利用する。render()関数というのを用意してドットのBufferを元にCanvasのメソッドを呼ぶようにする。こうすることで、drawRect関数でBufferである2次元配列から読み取ることで、青い四角形に隠されてしまう部分のドットの色を読み取ることができる。

gyazo.com

このやり方にするために関数を作成&変更する。 dot() 関数はBufferに書き込むまで、 render() でBufferの内容をcanvasへ描き込む。

// 400 * 400 の2次元配列で表現したドットのBuffer
const dotBuffer = new Array(400).fill(0).map(() => new Array(400));

// Bufferの内容をCanvasに1ドットずつ打つ
function render(context) {
  for (let y = 0; y < dotBuffer.length; y++) {
    const row = dotBuffer[y];
    for (let x = 0; x < row.length; x++) {
      const color = row[x];
      if (color) {
        context.fillStyle = `rgb(${color.red},${color.green},${color.blue})`;
        
        // アルファブレンディング後の色がセットされているので、canvas自体のalphaの考慮は不要
        // context.globalAlpha = color.alpha;

        context.fillRect(x*10, y*10, 10, 10);
      }
    }
  }
}

/**
 * Bufferにcolorを保存するだけ
 *
 * @param {int} x
 * @param {int} y
 * @param {object} color - {alpha: float, red: int, green: int, blue: int}
 */
function dot(x, y, color) {
  dotBuffer[y][x] = color;
}

これで準備OK。

色合成をする

合成後の色は以下の式で求める。不透明度が1に近づくほど前景色に近づいていくことが分かる。

合成後の色 = 背景色 * (1.0 - 不透明度) + 前景色 * 不透明度

合成後の色を求める関数を alphaBlend() として追加、 drawRect() から利用する。

function alphaBlend(fgColor, bgColor) {
  // 背景の色成分 * (1.0 - 不透明度) + 前景の色成分 * 不透明度
  const blend = (fg, bg, alpha) => bg * (1.0 - alpha) + fg * alpha;

  if (!bgColor) {
    bgColor = {alpha:1, red:255, green:255, blue:255};
  }
  const r = blend(fgColor.red, bgColor.red, fgColor.alpha);
  const g = blend(fgColor.green, bgColor.green, fgColor.alpha);
  const b = blend(fgColor.blue, bgColor.blue, fgColor.alpha);
  return {alpha:fgColor.alpha, red:r, green:g, blue:b};
}

function drawRect(x1, y1, x2, y2, color) {
  const xStart = Math.min(x1, x2);
  const yStart = Math.min(y1, y2);
  const xEnd = Math.max(x1, x2);
  const yEnd = Math.max(y1, y2);
  for (let y = yStart; y <= yEnd; y++) {
    for (let x = xStart; x <= xEnd; x++) {
      const bgColor = dotBuffer[y][x]; // ←追加
      const c = alphaBlend(color, bgColor); // ←追加
      dot(x, y, c);
    }
  }
}

青い四角形の不透明度を0.8として実行してみる。赤い折り紙に青いセロファンを重ねたようになるはず。

See the Pen DotRect by kariyayo (@kariyayo) on CodePen.


いい感じ!

おしまい

線と四角形をドットを打って描画してきました。1ドットずつ手続き的に打ってきましたが、GPUを使って並列実行してみたいですね。次回以降でやってみようかなぁ。