今回はHTMLのcanvas・JavaScriptを使って作る「お絵かきアプリ」シリーズの最後の記事です。
前回・前々回の記事で使用したコードをベースにしているので、まだ読んでいない方は以下の記事を読むことでより理解が深まるかと思います。
今回作成するものの完成形は次の画像になります。
上のアニメーション画像で新しく追加された機能は次の2つです。
- 「文字の太さ」スライダーで太さを変更できる
- canvas上にマウスカーソルが乗ると現在の線の太さが「○」の形で表示される
今回の記事で作成したコードと、公開されているアプリは以下のリンクからアクセスできます。
今回の記事を読むことで身につくスキルは次の通りです。
- HTMLの「 <input type=”range”> 」を使ったイベント処理
- style(CSS)の「 position: absolute 」を使ったHTML要素を重ねるやり方
- canvas上でマウスカーソルについてくる円の描画方法
- 2つのcanvasを重ねて使うテクニック
③と④に関してはゲームを作るときにも利用できるスキルになるので、「将来ゲーム作ってみたいな」と思っている方にとっても参考になる記事になります。
それでは、今回の記事は次の内容で話を進めていきます。
- 線の太さを変更できる機能を作る
- マウスカーソルについてくる円の描画用のcanvasを用意する
それでは1つ目の内容から見ていきましょう。
目次
線の太さを変更できる機能を作る
線の太さを変更するには、線の太さを表す「数値」を変更すれば良いので、今回使った「<input type=”range”>」でなくても、次のような選択肢もあります。
- 「<input type=”text”>」を使って数を打ち込む
- 「<input type=”radio”>」を使って値を選択できるようにする
- 「<select>, <option>」を使って値を選択できるようにする
どれを使うかは好みの問題になりますが、今回「<input type=”range”>」を選んだ理由は次の理由からです。
- マウス操作だけで完結させたかった
- 「0.1」単位で数の調整を行えるようにしたかったが、「<input type=”radio”>」と「<select>, <option>」を利用する場合はあらかじめ要素をたくさん準備する必要がある
- 「<input type=”range”>」を使うと、スライドバーで数を選択できて直感的だった
今回言っている「スライドバー」とは以下の画像でいうと、canvasの下にある値変更に使っているパーツのことです。
サンプルコードを使って解説
HTMLファイル、JavaScriptファイルの両方で、今回追加した機能の部分にのみコメントが書かれています。
今回の記事以外部分の機能を詳細を知りたい方は、前回・前々回の記事で使用するサンプルコードの中に細かくコメントで説明しているのでそちらを参考にしていただけたらと思います。
HTML
それではHTMLの内容から見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="css/colorjoe.css"> <title>お絵かきアプリ</title> </head> <body> <h1>お絵かきアプリ</h1> <div> <span id="layerd-canvas-area"> <!-- "position: absolute;" を利用することで2つのcanvasを重ねている --> <!-- 2つのcanvasを重ねている理由は線の描画と、線の太さを表現する「○」を同時に行うため。 --> <canvas id="draw-area" width="400px" height="400px" style="border: 1px solid #000000; position: absolute;"></canvas> <canvas id="line-width-indicator" width="400px" height="400px" style="border: 1px solid #000000;"></canvas> </span> <span id="color-palette"></span> </div> <!-- 線の太さを変更するために <input type="range">を利用する --> <!-- ドキュメント: https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/range --> <!-- 参考になるサイト: https://itsakura.com/html5-range --> <div> 文字の太さ <input id="range-selector" type="range" value="5" min="1" max="10" step="0.1"> <!-- 現在の線の太さを表す数値を表示するための要素 --> <!-- input要素のスライドを動かすたびに値が更新される --> <span id="line-width">5</span> </div> <div> <button id="clear-button">全消し</button> </div> <div> <button id="eraser-button">消しゴムモード</button> </div> <script src="./js/colorjoe.min.js"></script> <script src="./js/main.js"></script> </body> </html> |
上記のコードのうち、線の太さ変更に関わる部分は、32~44行目の部分になり、2つのパーツに分けられます。
- 34~40行目: 「<input type=”range”>」 を使ってスライドバーを用意
- 43行目: 「<span id=”line-width”>」で現在のスライドバーで選択している値を目視できるようにする
①の「<input type=”range”>」では、スライドバーの基本設定を行っています。それぞれの意味は次のとおりです。
- id: JavaScriptでこのinput要素を操作するための値
- type: range(スライドバー)を使うことを宣言
- value: デフォルト値
- min: スライドバーで選択できる最小値
- max: スライドバーで選択できる最大値
- step: 値の細かさの設定(0.01まで可能(ドキュメントより))
スライドバーを動かすことで値の変更を行うことが出来るのですが、「<input type=”range”>」だけだとページ上に現在の値を表示することが出来ません。
そこで、現在の値を表示するための要素として②の「<span id=”line-width”>」を追加して、JavaScript経由で値を更新出来るようにしています。
JavaScript
それでは、どのように値を更新しているのかJavaScriptのコードを確認しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
// https://github.com/tsuyopon-xyz/drawing_app_part2/blob/master/main.js // 上記のコードを元に以下の追加機能を追加します。 // - 線の太さ変更する機能 // // 元々書かれてた説明のコメントは削除しました。理由は次のとおりです。 // - 今回の変更差分の説明コメントのみにすることで、どの部分で変更があったかわかりやすくするため window.addEventListener('load', () => { const canvas = document.querySelector('#draw-area'); 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); } 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'); // 線の太さを記憶している変数の値を更新する currentLineWidth = rangeSelector.value; // "input"イベントをセットすることでスライド中の値も取得できるようになる。 // ドキュメント: https://developer.mozilla.org/ja/docs/Web/HTML/Element/Input/range rangeSelector.addEventListener('input', event => { const width = event.target.value; // 線の太さを記憶している変数の値を更新する currentLineWidth = width; // 更新した線の太さ値(数値)を<input id="range-selector" type="range">の右側に表示する textForCurrentSize.innerText = width; }); } initEventHandler(); initColorPalette(); // 文字の太さの設定を行う機能を有効にする initConfigOfLineWidth(); }); |
上記のコードの内、線の太さの更新に該当する箇所は以下の部分です。
- 21行目: 線の太さの値を保持する「currentLineWidth」変数
- 135~153行目: 線の太さの値の更新処理をまとめている「initConfigOfLineWidth」関数
- <input type=”range”>、<span id=”line-width”>のDOM操作
- 太さ情報を保持している「currentLineWidth」変数の値の更新
- 159行目: 「initConfigOfLineWidth」関数を実行して、線の太さの値の更新処理を有効化
①で用意した「currentLineWidth」変数を使って線の太さを表現して、値が小さいほど線が細くなり、値が大きいほど線が太くなります。
この「currentLineWidth」変数は線を引く時に実行されるdraw関数内の29行目で線の太さを定義する時に使われます。
②では、HTML内で値変更用に用意した「<input type=”range”>」、現在選択している数値を表示するための「<span id=”line-width”>」のDOM操作と、値に変更があったときのイベント処理の定義をしています。
144行目のaddEventListenerで「’input’」イベントをセットすることで、スライドを動かしている最中も値を取得することが出来ます。
次のアニメーション画像は、スライドバーを動かしている時に上記145行目のwidthの値を「console.log」を使って確認しているところです。
③で行っていることは②で用意した「initConfigOfLineWidth」を呼び出して有効化しているだけです。
以上で線の太さを変更する実装は完了です。
次にマウスカーソルについてくる円の描画方法について説明します。
マウスカーソルについてくる円の描画用のcanvasを用意する
「マウスカーソルについてくる円」とはちょっと言葉だけだとわかりにくいので、次の画像を見てください。
上の画像を見ると、左側にあるcanvasにマウスカーソルが乗った時に「○(円)」がついてきているのがわかるかと思います。
この「○(円)」は現在の線の太さを表現しているのですが、この機能を実現するためにもう一つ別のcanvasを用意しています。
以下のHTMLは先程のHTMLファイルの中からcanvasの部分だけ抜粋したものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span id="layerd-canvas-area"> <!-- "position: absolute;" を利用することで2つのcanvasを重ねている --> <!-- 2つのcanvasを重ねている理由は線の描画と、線の太さを表現する「○」を同時に行うため。 --> <canvas id="draw-area" width="400px" height="400px" style="border: 1px solid #000000; position: absolute;"></canvas> <canvas id="line-width-indicator" width="400px" height="400px" style="border: 1px solid #000000;"></canvas> </span> |
上記2つのcanvasの役割はそれぞれの次のとおりです。
- 4~8行目のcanvas: 線を書くためのもの
- 9~13行目のcanvas: 「○(円)」を描画するためのもの
2つのcanvasを使う理由
なぜ役割がことなるだけで2つのcanvasが必要になるのでしょうか?
①の機能に関しては、「全消し」ボタンを押さない限りは、過去に引いた線を残し続ける必要があります。
線を引くたびに過去に書いた線が消されたらただの点の移動になってしまうからです。
しかし、②の機能に関しては、過去に描画した「○(円)」をマウスカーソルが移動するたびに削除する必要があります。
過去に描画した「○(円)」を削除しなかった場合は次の画像のように「○(円)」が足跡のように残ってしまいます。
上記までに話した内容をまとめると次の通りです。
- 線を引くときは過去の描画を削除してはいけない。削除するとただの点の移動になる。
- 「○(円)」を描画するとき、過去の「○(円)」を消さないと、足跡のように残り続けてします。
このように、①線を書く機能と、②「○(円)」を描画する機能で正反対のことをしなければいけないのです。
その理由より「○(円)」描画用にcanvasを追加しました。
2つのcanvasを重ねる
2つのcanvasを重ねることで、マウスドラッグを開始した時に、「○(円)」の位置から線を書くことが出来ます。
しかし、どのようにして2つのHTML要素を重ねているのでしょうか?
CSSの「position: absolute;」を使うことで実現できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span id="layerd-canvas-area"> <!-- "position: absolute;" を利用することで2つのcanvasを重ねている --> <!-- 2つのcanvasを重ねている理由は線の描画と、線の太さを表現する「○」を同時に行うため。 --> <canvas id="draw-area" width="400px" height="400px" style="border: 1px solid #000000; position: absolute;"></canvas> <canvas id="line-width-indicator" width="400px" height="400px" style="border: 1px solid #000000;"></canvas> </span> |
今回はサンプルコードなので8行目のcanvas要素のstyle値に直接指定しています。
JavaScriptで○(円)を描画する
先程共有したJavaScriptのコードの内、「○(円)」の描画に関係する部分のコードだけ抜粋します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// 現在のマウスの位置を中心に、現在選択している線の太さを「○」で表現するために使用するcanvas const canvasForWidthIndicator = document.querySelector('#line-width-indicator'); const contextForWidthIndicator = canvasForWidthIndicator.getContext('2d'); // <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 initEventHandler() { // 省略 // 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); }); } |
「○(円)」を描画するために必要なことは大きく分けると以下の3つになります。
- 2~3行目: 円描画用のcanvas情報取得
- 7~28行目: 円描画のための「showLineWidthIndicator」関数の用意
- 30~65行目: canvasを2つ使うようになったため、マウスイベントをcanvas2つの親要素である「<span id=”layerd-canvas-area”>」に定義する
①は、線を書くためのcanvasと同じように、円描画用のcanvas情報から描画するために必要なcontext情報を取得します。
「context」とはcanvasに図や線などを描画するのに必要なものという認識で大丈夫です。
②は、線引くときと同じようにcanvasに描画を行う処理をまとめたものですが、2点線を引くときと異なる部分があります。
上記2つの機能により、「showLineWidthIndicator」関数が実行されるたびに、canvasがリセットされた後に、マウスカーソル位置を中心に円の描画が実現されます。
そして、③で行っているマウスイベントによって実行される56行目のコールバック関数内で、②で定義した「showLineWidthIndicator」関数は実行されます。
前回までは、線を書くためのcanvasに対してマウスイベントを定義していましたが、今回の追加機能により2つのcanvasを利用することになったため、
2つのcanvasの共通の親要素である「<span id=”layerd-canvas-area”>」に対してマウスイベントを定義するように変更しました。
仮に前回から変更せずに、線をかくためのcanvasに対してイベントを定義していたとしても同じように動くかと思いますが、以下の理由より親要素である「<span id=”layerd-canvas-area”>」に対して変更しました。
- 1つのcanvasに対してのイベント定義の場合、指定したcanvasのみを対象とする意味合いになる
- 親要素に対してイベントを定義すると、親が内包している子要素(今回の場合は2つのcanvas)もイベント対象に含まれる意味合いになる
上記の理由のように、ただ動くことだけを考えるのではなく、イベントで実行する処理内容の意味まで考えて適切だと思われる要素に対してイベントを定義しています。
まとめ
あらためて、今まで作成したお絵かきアプリの内容を振り返ってみましょう。
以下にこれまで説明してきたコードのGithubレポジトリのリンクと、実際に機能を確認できるリンクを貼っておきます。
これまでcanvasで出来ることの説明として「お絵かき」アプリを題材にして説明しましたが、canvasで出来ることたくさんありその一例は次のとおりです。
- ゲーム
- Webビデオ使ったアプリ
ぜひ、HTMLのcanvasを使ったアプリケーションづくりにもチャレンジしてみてください^^