プログラムでピアノの鍵盤を描画する方法

# 1. 概要
ピアノの鍵盤を描画したいとしよう。座標に基づいて動的に描画するためには若干の計算が必要になる。

![スクリーンショット 2019-08-01 21.47.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58850/60a5419c-17a8-3b8f-c885-11f87a0cc7df.png)

以下では、計算にあたっての考え方とJavaScriptによる実装を紹介する。描画はd3.jsを用いてSVGを生成することによって行うが、考え方自体はフレームワークに限定されない。


# 2. 考え方
描画のために必要な情報は、各鍵盤の大きさと位置である。

**(1) 白鍵と黒鍵の判定**
白鍵も黒鍵も混ぜ合わせ、低音側から高音側の順に0, 1, 2, ..., 87（88鍵の場合）というindexを鍵盤ごとに与えるとする。この時、オクターブごとの周期性により、indexを12で割った余りによって白鍵・黒鍵の判定が可能になる。

**(2) 大きさの計算**
外枠の大きさが決まっている時、大きさに関しては以下のように定めることができる。0.何倍というのは好みに応じて修正すれば良い。

- 白鍵の幅は、全鍵盤中の白鍵の数によって決まる
- 黒鍵の幅は、白鍵の幅の0.何倍かで決まる
- 白鍵の高さは、外枠の高さそのもの
- 黒鍵の高さは、白鍵の高さの0.何倍かで決まる

**(3) 位置の計算**
鍵盤のindexが与えられた時、鍵盤の位置は以下のように定めることができる。

- 縦方向の位置は黒鍵・白鍵ともに固定値
- 白鍵の横方向の位置は、index番目よりも低音側にある白鍵の数の合計数と、白鍵1つあたりの横幅によって決まる。これらは先述の(1)(2)から計算できる
- 黒鍵の横方向の位置も同様。白鍵の間に来るように黒鍵幅の1/2のオフセットが必要になることに注意

# 3. 実装例
JavaScriptで実装しているため、以下からブラウザで動かすことができる。
https://observablehq.com/@sw1227/simple-piano-keyboard

以下のようなPiano関数を作成する

```javascript
// Create a piano object that contains informations to draw piano keyboard
function Piano(width, height, widthRatio, heightRatio) {
  const numKeys = 88; // # of keys (black/white) in a piano
  // Function to check if keyIndex-th key is white or not(=black)
  const isWhite = keyIndex => ![1, 4, 6, 9, 11].includes(keyIndex % 12);
  // # of white keys with lower index than keyIndex-th key
  const numLowerWhites = keyIndex => (
    d3.range(keyIndex).reduce((acc, val) => (isWhite(val) ? acc+1 : acc), 0)
  );
  // Total # of white keys
  const numWhiteKeys = numLowerWhites(numKeys);

  // Dimension of keys
  const whiteWidth = width / numWhiteKeys;
  const blackWidth = whiteWidth * widthRatio;

  return d3.range(numKeys).map(i => {
    const offset = isWhite(i) ? 0 : -blackWidth/2;
    return {
      index: i,
      type: isWhite(i) ? "white" : "black",
      // Piano coordinate: x: lower to higher key, y: near to far
      coord: {
        x: {
          min: whiteWidth * numLowerWhites(i) + offset,
          max: whiteWidth * numLowerWhites(i) + offset + (isWhite(i) ? whiteWidth : blackWidth)
        },
        y: {
          min: isWhite(i) ? 0 : (1 - heightRatio) * height,
          max: height
        }
      }
    };
  })
}
```

88個の鍵盤の情報（座標・黒鍵か白鍵かなど）を持った配列を出力するため、これを利用して描画処理を行えば良い。d3.jsを用いる場合、鍵盤情報の配列を元にSVGの`<rect>`要素を生成する。

```javascript
  const svg = d3.create("svg")
      .attr("width", width)
      .attr("height", height);

  // Draw white keys first: cannot set z-index for svg
  svg.selectAll("rect.white").data(piano.filter(key => key.type=="white"))
    .enter()
  .append("rect")
    .attr("x", key => key.coord.x.min)
    .attr("y", key => key.coord.y.min)
    .attr("width", key => key.coord.x.max - key.coord.x.min)
    .attr("height", key => key.coord.y.max - key.coord.y.min)
    .attr("stroke", "#ccc")
    .attr("fill", "#fff")
    .attr("stroke-width", 1);

  // Draw black keys
  svg.selectAll("rect.black").data(piano.filter(key => key.type=="black"))
    .enter()
  .append("rect")
    .attr("x", key => key.coord.x.min)
    .attr("y", 0)
    .attr("width", key => key.coord.x.max - key.coord.x.min)
    .attr("height", key => key.coord.y.max - key.coord.y.min)
    .attr("stroke", "#ccc")
    .attr("fill", "#444")
    .attr("stroke-width", 1);
```

# 4. 応用
鍵盤情報の配列を作成してdata-drivenに描画するアプローチは、以下のような発展系も考えられる。

- 鍵盤情報の配列の中にMIDIノート番号も保持させて、WebMIDI APIと組み合わせる
- data-drivenに描画する際、Web Audio APIによる音の生成をコールバックとし、演奏を可能にする

色々試してみよう。