ブラウザでお絵かき＆保存をしよう！

#ブラウザで絵を書きたいときってありませんか？

この記事では、<a href='https://tsuyopon.xyz/2018/09/15/how-to-create-drawing-app-part2/'>HTMLのcanvasとJavaScriptでお絵かきアプリ【色・消しゴム追加編】</a>に、独自にいくつか機能を加えたものを紹介します。コピペすれば導入できるようにしてあります。
##完成するとこんな感じ
<img width="1000" alt="スクリーンショット 2020-04-19 3.05.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449865/e1474827-7380-8331-1d2a-4d7c5ff1b0d2.png">

##追加機能、改善点
- 文字の太さを、調節バーに加えて、**直接数字でもいじれる**ようにしたこと。

- 文字の太さの選択範囲を**５倍**拡大したこと(1〜10 → 1〜50)。

- canvasで描いた絵を**画像としてダウンロード**できるようにしたこと。
- カラーパレットを実装用するための<a href='https://github.com/bebraw/colorjoe'>「colorjoe」</a>という**ライブラリに必要なモノ全て**を、CDNで読み込むようにしたこと。


##ソースコードは以下
```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/colorjoe@4.1.1/css/colorjoe.min.css"
    />    <!-- colorjoeのcssをダウンロード-->

  </head>
  <body>
    <h1>お絵かきしよう</h1>
    <div id="all-container">
      <span id="layerd-canvas-area">
        <canvas
          id="draw-area"
          width="600px"
          height="600px"
          style="border: 1px solid #000000; position: absolute;"
        ></canvas>
        <canvas
          id="line-width-indicator"
          width="600px"
          height="600px"
          style="border: 1px solid #000000;"
        ></canvas>
      </span>
      <div id="right">
        <div>
          <div>
            <button id="clear-button" class="menu">全消し</button>
          </div>
          <div>
            <button id="eraser-button" class="menu">消しゴムモード</button>
          </div>
          <div>
            文字の太さ
            <span id="line-width">5</span>
          </div>
          <input
            id="range-selector"
            type="range"
            value="5"
            min="1"
            max="50"
            step="0.1"
          />

          <input
            id="line-width-number-field"
            type="number"
            value="5"
            min="1"
            max="50"
            step="0.1"
          />
        </div>
        <div>
          <span id="color-palette"></span>
        </div>
      </div>
    </div>

    <a href="#" id="download" download="canvas.jpg">
      お絵かきをダウンロード
    </a>
    <style>
      #all-container {
        display: flex;
        flex-direction: row;
      }
      #right {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
      }
      .menu {
        margin-bottom: 20px;
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/colorjoe@4/dist/colorjoe.min.js"></script>
    <script src="oekaki.js"></script>
  </body>
</html>
 ```

```javascript:oekaki.js
window.addEventListener("load", () => {
  const canvas = document.querySelector("#draw-area");
  ctx = canvas.getContext("2d");
  ctx.fillStyle = "#FFFFFF"; //筆に白い絵の具をつけて
  ctx.fillRect(0, 0, 600, 600); //四角を描く
  const context = canvas.getContext("2d");

  // 現在のマウスの位置を中心に、現在選択している線の太さを「○」で表現するために使用するcanvas
  const canvasForWidthIndicator = document.querySelector(
    "#line-width-indicator"
  );
  const contextForWidthIndicator = canvasForWidthIndicator.getContext(
    "2d"
  );

  const lastPosition = { x: null, y: null };
  let isDrag = false;
  let currentColor = "#000000";

  // 現在の線の太さを記憶する変数
  // <input id="range-selector" type="range"> の値と連動する
  let currentLineWidth = 1;

  function draw(x, y) {
    if (!isDrag) {
      return;
    }
    context.lineCap = "round";
    context.lineJoin = "round";
    context.lineWidth = currentLineWidth;
    context.strokeStyle = currentColor;
    if (lastPosition.x === null || lastPosition.y === null) {
      context.moveTo(x, y);
    } else {
      context.moveTo(lastPosition.x, lastPosition.y);
    }
    context.lineTo(x, y);
    context.stroke();

    lastPosition.x = x;
    lastPosition.y = y;
  }

  // <canvas id="line-width-indicator"> 上で現在のマウスの位置を中心に
  // 線の太さを表現するための「○」を描画する。
  function showLineWidthIndicator(x, y) {
    contextForWidthIndicator.lineCap = "round";
    contextForWidthIndicator.lineJoin = "round";
    contextForWidthIndicator.strokeStyle = currentColor;

    // 「○」の線の太さは細くて良いので1で固定
    contextForWidthIndicator.lineWidth = 1;

    // 過去に描画「○」を削除する。過去の「○」を削除しなかった場合は
    // 過去の「○」が残り続けてします。(以下の画像URLを参照)
    // https://tsuyopon.xyz/wp-content/uploads/2018/09/line-width-indicator-with-bug.gif
    contextForWidthIndicator.clearRect(
      0,
      0,
      canvasForWidthIndicator.width,
      canvasForWidthIndicator.height
    );

    contextForWidthIndicator.beginPath();

    // x, y座標を中心とした円(「○」)を描画する。
    // 第3引数の「currentLineWidth / 2」で、実際に描画する線の太さと同じ大きさになる。
    // ドキュメント: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc
    contextForWidthIndicator.arc(
      x,
      y,
      currentLineWidth / 2,
      0,
      2 * Math.PI
    );

    contextForWidthIndicator.stroke();
  }

  function clear() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    ctx = canvas.getContext("2d");
    ctx.fillStyle = "#FFFFFF"; //筆に白い絵の具をつけて
    ctx.fillRect(0, 0, 600, 600); //四角を描く
  }

  function dragStart(event) {
    context.beginPath();

    isDrag = true;
  }

  function dragEnd(event) {
    context.closePath();
    isDrag = false;
    lastPosition.x = null;
    lastPosition.y = null;
  }

  function initEventHandler() {
    const clearButton = document.querySelector("#clear-button");
    const eraserButton = document.querySelector("#eraser-button");
    clearButton.addEventListener("click", clear);
    eraserButton.addEventListener("click", () => {
      currentColor = "#FFFFFF";
    });

    // layeredCanvasAreaは2つのcanvas要素を保持している。2つのcanvasはそれぞれ以下の役割を持つ
    //
    // 1. 絵を書くためのcanvas
    // 2. 現在のマウスの位置を中心として、太さを「○」の形で表現するためのcanvas
    //
    // 1と2の機能を1つのキャンパスで共存することは出来ない。
    // 共存できない理由は以下の通り。
    //
    // - 1の機能は過去に描画してきた線の保持し続ける
    // - 2の機能は前回描画したものを削除する必要がある。削除しなかった場合は、過去の「○」が残り続けてしまう。(以下の画像URLを参照)
    //   - https://tsuyopon.xyz/wp-content/uploads/2018/09/line-width-indicator-with-bug.gif
    //
    // 上記2つの理由より
    // - 1のときはcontext.clearRectを使うことが出来ず
    // - 2のときはcontextForWidthIndicator.clearRectを使う必要がある
    const layeredCanvasArea = document.querySelector(
      "#layerd-canvas-area"
    );

    // 元々はcanvas.addEventListenerとしていたが、
    // 2つのcanvasを重ねて使うようになったため、親要素である <span id="layerd-canvas-area">に対して
    // イベント処理を定義するようにした。
    layeredCanvasArea.addEventListener("mousedown", dragStart);
    layeredCanvasArea.addEventListener("mouseup", dragEnd);
    layeredCanvasArea.addEventListener("mouseout", dragEnd);
    layeredCanvasArea.addEventListener("mousemove", (event) => {
      // 2つのcanvasに対する描画処理を行う

      // 実際に線を引くcanvasに描画を行う。(ドラッグ中のみ線の描画を行う)
      draw(event.layerX, event.layerY);

      // 現在のマウスの位置を中心として、線の太さを「○」で表現するためのcanvasに描画を行う
      showLineWidthIndicator(event.layerX, event.layerY);
    });
  }

  function initColorPalette() {
    const joe = colorjoe.rgb("color-palette", currentColor);
    joe.on("done", (color) => {
      currentColor = color.hex();
    });
  }

  // 文字の太さの設定・更新を行う機能
  function initConfigOfLineWidth() {
    const textForCurrentSize = document.querySelector("#line-width");
    const rangeSelector = document.querySelector("#range-selector");
    const numberField = document.getElementById(
      "line-width-number-field"
    );
    // 線の太さを記憶している変数の値を更新する
    currentLineWidth = rangeSelector.value;

   // 線を<input type='number'>からも更新できるようにする。
    numberField.addEventListener("input", (event) => {
      const width = event.target.value;
      // 線の太さを記憶している変数の値を更新する
      currentLineWidth = width;
      rangeSelector.value = width;
      // 更新した線の太さ値(数値)を<input id="range-selector" type="range">の右側に表示する
      textForCurrentSize.innerText = width;
    });
    // "input"イベントをセットすることでスライド中の値も取得できるようになる。
    // ドキュメント: https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/range

    rangeSelector.addEventListener("input", (event) => {
      const width = event.target.value;
      numberField.value = width;
      // 線の太さを記憶している変数の値を更新する
      currentLineWidth = width;

      // 更新した線の太さ値(数値)を<input id="range-selector" type="range">の右側に表示する
      textForCurrentSize.innerText = width;
    });
  }

  initEventHandler();
  initColorPalette();

  // 文字の太さの設定を行う機能を有効にする
  initConfigOfLineWidth();
  const button = document.getElementById("download");
  button.onclick = function() {
    let canvas = document.getElementById("draw-area");
    console.dir(canvas)
    let base64 = canvas.toDataURL("image/jpeg");

    document.getElementById("download").href = base64;
  };
});
```
お絵かきをお楽しみください！