8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ブラウザでお絵かき&保存をしよう!

Last updated at Posted at 2020-04-18

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

この記事では、HTMLのcanvasとJavaScriptでお絵かきアプリ【色・消しゴム追加編】に、独自にいくつか機能を加えたものを紹介します。コピペすれば導入できるようにしてあります。

完成するとこんな感じ

スクリーンショット 2020-04-19 3.05.17.png

追加機能、改善点

  • 文字の太さを、調節バーに加えて、直接数字でもいじれるようにしたこと。

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

  • canvasで描いた絵を画像としてダウンロードできるようにしたこと。

  • カラーパレットを実装用するための「colorjoe」というライブラリに必要なモノ全てを、CDNで読み込むようにしたこと。

ソースコードは以下

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>
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;
  };
});

お絵かきをお楽しみください!

8
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?