ソースコードで学ぶWebプログラミング

お絵描きツールの作り方

ブラウザからマウスで絵を描くことが出来るツールを開発します。 絵の描画入力は、HTML5のcanvas要素が用いられます。描画処理は、javascriptで実装します。

スマホやタブレットにも対応してみます。

学べること

・canvasの扱い方 ・マウスイベントの扱い方 ・HTMLカラーコードについて ・canvas上の描画データの保存方法 ・レスポンシブデザイン

お絵描きツールに必須の機能とは

プログラムを作成するために、まず要件定義を行います。

それでは「お絵描きツール」に必須の機能をピックアップします。

1.描画操作を受け取る 2.描画操作を「描画データ」に変換する 3.描画データを「画像」として出力する

機能としては網羅できているようです。さらに詳細を決めていきます。 描画を行う canvas の保存処理は、toDataURL() にてpng、jpegに変換できるようです。 今回はpng画像で保存することにします。

・マウスまたはタッチ操作で線を引く ・一覧から「線の太さ」「線の色」を選択できる ・「線の太さ」「線の色」を細かく設定できる ・描画した状態をクリアできる ・描画した状態をpng画像で保存する

ここまでの要件をまとめてみます。機能に対して、関連する詳細を当てはめていきます。

1.描画操作を受け取る ・マウスまたはタッチ操作で線を引く 2.描画操作を「描画データ」に変換する ・一覧から「線の太さ」「線の色」を選択できる ・「線の太さ」「線の色」を細かく設定できる ・描画した状態をクリアできる 3.描画データを「画像」として出力する ・描画した状態をpng画像で保存する

お絵描きツールの要件をコード化する

それではコーディングに移ります。

1.描画操作を受け取る

描画操作は、マウスまたはタッチ操作を受け取って行うので、javascriptにて それぞれのイベント内で処理を行うことになります。

また「線の太さ」や「線の色」を選択可能にするため、ボタンのようなものを HTMLタグで作成し、クリックを受け付ければ、処理できそうです。

<!-- canvas要素 --> <div id="main"> <canvas id="cv"></canvas> <div id="ctrl"> <!-- 「線の太さ」ボタン群 --> <span class="wds cur" wd="1"><span id="w1"></span></span> <span class="wds" wd="2"><span id="w2"></span></span> <span class="wds" wd="3"><span id="w3"></span></span> <span class="wds" wd="4"><span id="w4"></span></span> <span class="wds" wd="5"><span id="w5"></span></span> <span class="wds" wd="6"><span id="w6"></span></span> <span class="wds" wd="7"><span id="w7"></span></span> <span class="wds" wd="8"><span id="w8"></span></span> <span class="wds" wd="9"><span id="w9"></span></span> <span class="wds" wd="10"><span id="w10"></span></span> <span class="wds" wd="11"><span id="w11"></span></span> <span class="wds" wd="12"><span id="w12"></span></span> <span class="wds" wd="13"><span id="w13"></span></span> <!-- 「線の太さ」選択枠 --> <input type="number" id="width" min="1" value="20"><br> <!-- 「線の色」ボタン群 --> <span class="cls cur" cl="#000"><span id="c1"></span></span> <span class="cls" cl="#fff"><span id="c2"></span></span> <span class="cls" cl="#f00"><span id="c3"></span></span> <span class="cls" cl="#080"><span id="c4"></span></span> <span class="cls" cl="#00f"><span id="c5"></span></span> <span class="cls" cl="#800"><span id="c6"></span></span> <span class="cls" cl="#fd0"><span id="c7"></span></span> <span class="cls" cl="#fcc"><span id="c8"></span></span> <span class="cls" cl="#888"><span id="c9"></span></span> <span class="cls" cl="#000"><span id="c10"></span></span> <!-- 「線の色」選択枠 --> <input type="color" id="color"> <input type="button" id="clear" value="クリア"> <input type="button" id="save" value="保存"> </div> </div>

やっている事

・canvas id="cv" で描画要素を指定 ・class="wds" を「線の太さ」群の特定用に設定 ・class="cur" で現在の選択状態を表現できるように ・wd="数" の属性で「線の太さ」を指定 ・span id="w数値" で「線の太さ」の種別を表現できるように ・input type="number" id="width で「線の太さ」を入力 ・class="cls" を「線の色」群の特定用に設定 ・cl="カラーコード" で「線の色」を指定 ・span id="c数値" で「線の色」の種別を表現できるように ・input type="color" で「線の色」を詳細選択できるように ・type="button" id="clear" でクリアボタンを設置 ・type="button" id="save" で保存ボタンを設置

id属性は、処理する上でその要素を特定するために、class属性は複数の要素を同じものとして 扱えるように設定しています。このidやclassは、cssとjavascriptで利用します。 cssは下記のようになります。

#main { margin: 50px auto; width: 485px; } #cv { border: 1px solid #000; cursor: pointer; width: 100%; } input[type='number'],input[type='button'] { padding: 5px; box-sizing: border-box; } #ctrl { margin-top: 2px; line-height: 35px; } .wds { display: inline-block; height: 29px; width: 29px; cursor: pointer; vertical-align: bottom; } .wds span { display: inline-block; margin: 2px; width: 23px; border-top-style: solid; border-top-color: #000; } #w1 { border-top-width: 1px; } #w2 { border-top-width: 2px; } #w3 { border-top-width: 3px; } #w4 { border-top-width: 4px; } #w5 { border-top-width: 5px; } #w6 { border-top-width: 6px; } #w7 { border-top-width: 7px; } #w8 { border-top-width: 8px; } #w9 { border-top-width: 9px; } #w10 { border-top-width: 10px; } #w11 { border-top-width: 11px; } #w12 { border-top-width: 12px; } #w13 { border-top-width: 13px; } #width { width: 50px; } span { box-sizing: border-box; } .cls { display: inline-block; height: 29px; width: 29px; border: 1px solid #aaa; cursor: pointer; vertical-align: bottom; } .cls span { display: inline-block; margin: 2px; height: 23px; width: 23px; border: 1px solid #aaa; } #c1 { background: #000; } #c2 { background: #fff; } #c3 { background: #f00; } #c4 { background: #080; } #c5 { background: #00f; } #c6 { background: #800; } #c7 { background: #fd0; } #c8 { background: #fcc; } #c9 { background: #888; } #c10 { background: #000; } .cur { border: 1px solid #f55; }

ここまでで下のような画面が出来上がります。

描画を操作する画面

すでにスマホ用のレスポンシブを意識した構成になっていますが、 レスポンシブ対応の具体的なタグやcssは下記のようになります。

<!-- 画面サイズを設定 --> <meta name="viewport" content="width=device-width, initial-scale=1"> @media screen and (max-width: 500px){ #main { width: 90%; } #clear { width: 100%; } #save { width: 100%; } }

スマホ用の画面の様子

2.描画操作を「描画データ」に変換する

続いてブラウザでの操作を描画データとして反映する処理を実装します。

/* 描画の開始 */ var wStart = function(e){ /* スマホで画面がずれないように */ e.preventDefault(); /* 描画モードに */ w = true; ctx.beginPath(); /* スマホ、タブレット以外 */ if (typeof e.touches=='undefined') { ctx.moveTo(e.offsetX, e.offsetY); /* スマホ、タブレット */ }else{ var t = e.touches[0]; /* 描画位置のズレを修正 */ ctx.moveTo(t.pageX-left, t.pageY-top); } } /* マウス押下時 */ c.onmousedown = wStart; /* タッチ開始時 */ c.ontouchstart = wStart;

やっている事

・e.preventDefault() でイベントをキャンセル ・.beginPath() 描画パスの初期化 ・typeof e.touches=='undefined' でスマホ、タブレット判定 ・.moveTo() で描画の開始点を設定 ・.pageX-left、.pageY-top で位置ズレを修正 ・.onmousedown =、.ontouchstart = でイベントの処理内容を登録

描画領域へのクリックでは問題は起こりませんが、タッチイベントでは 画面がスクロールされる事があるため、イベントのキャンセルを行います。

また描画位置の指定で、pageX、pageYはページ全体での位置となるため、 描画領域内での位置座標とするために、描画領域の座標分を減算します。

/* ラインを描画 */ var wLine = function(e){ /* 描画モードの時 */ if (w) { /* スマホ、タブレット以外 */ if (typeof e.touches=='undefined') { ctx.lineTo(e.offsetX, e.offsetY); /* スマホ、タブレット */ }else{ var t = e.touches[0]; /* 描画位置のズレを修正 */ ctx.lineTo(t.pageX-left, t.pageY-top); } ctx.stroke(); } } /* マウス移動時 */ c.onmousemove = wLine; /* タッチ移動時 */ c.ontouchmove = wLine;

やっている事

・.lineTo() で描画する線のの終点を設定 ・.stroke() で線を描画 ・.onmousemve =、.ontouchmove = でイベントの処理内容を登録

これで描画領域内でマウスを動かすだけで線が引かれるようになりました。 ただし、現在の状態では一度描画を始めると線を引かずにマウスを移動させることが出来ないので、 描画を終了する処理を実装します。

/* 描画の終了 */ var wStop = function(){ w = false; } /* マウスボタンを離した時 */ c.onmouseup = wStop; /* タッチ終了時 */ c.ontouchend = wStop;

やっている事

・w = false で描画モードを終了

続いて描画した線をすべて消去する処理を実装してみます。

/* クリアボタン押下時 */ cr.onclick = function(){ ctx.clearRect(0, 0, c.width, c.height); }

やっている事

・.clearRect() で描画領域をクリア

ここまでで描画処理の実装は完了しました。次は描画する「線の太さ」や「線の色」の設定が行えるようにします。

まずボタン群の一覧からクリックで対象を選択する時に一旦、選択状態を解除する処理を実装します。 選択状態はclassのcurにて表現されているため、ボタン群のclass属性を他の未選択ボタンと同じ状態になるように 初期化します。

/* 選択を未選択状態に */ var clearCs = function(cs,def){ for(var i=0,len=cs.length;i<len;i++){ cs[i].setAttribute('class',def); } }

やっている事

・.setAttribute() でclass属性を初期化

選択状態の解除が必要となる箇所は「線の太さ」選択、「線の色」選択の2種類あります。 そのため2タイプに対応できるようにコーディングします。 具体的には、対象の要素群と対象のclass初期値を受け取れるようにしています。

次に「線の太さ」関連の処理を実装します。

/* 線の太さ入力欄に変更があった時 */ wd.onchange = function(){ clearCs(wds,'wds'); ctx.lineWidth = this.value; this.setAttribute('class','cur'); } /* 線選択枠のクリックイベントの登録 */ for(var i=0,len=wds.length;i<len;i++){ wds[i].onclick = function(){ /* 線の太さ入力欄の選択状態を解除 */ wd.removeAttribute('class'); clearCs(wds,'wds'); ctx.lineWidth = this.getAttribute('wd'); this.setAttribute('class','wds cur'); } }

やっている事

・clearCs() で選択状態をクリア ・.lineWidth = で「線の太さ」を設定 ・.setAttribute('class','cur') で対象を選択状態に ・wds[i].onclick = でクリック時のイベントを登録

次は「線の色」選択関連の処理です。ほぼ同じ処理ですが、色の詳細設定を行った時に、 色データをログとして保存できるようにしてみます。 色の詳細選択ボタンの右横の10番目のボタンに保存するようにします。

/* 色ウィンドウから選択された時 */ cl.onchange = function(){ /* 選択した色を保存するボタン */ var p10 = c10.parentNode; clearCs(cls,'cls'); ctx.strokeStyle = cl.value; c10.style.background = cl.value; p10.setAttribute('cl',cl.value); p10.setAttribute('class','cls cur'); } /* 色選択枠のクリックイベントの登録 */ for(var i=0,len=cls.length;i<len;i++){ cls[i].onclick = function(){ clearCs(cls,'cls'); ctx.strokeStyle = this.getAttribute('cl'); this.setAttribute('class','cls cur'); } }

やっている事

・.strokeStyle = で「線の色」を設定 ・.style.background = で選択した「線の色」をボタン上に反映

3.描画データを「画像」として出力する

最後は描画データをpng画像として保存できるようにします。

/* 保存ボタン押下時 */ sv.onclick = function(){ /* 画像のURL表現からデータ部を取出、Base64デコード */ var data = atob( c.toDataURL().replace(/^[^,]*,/,'') ), /* Bufferデータ配列に変換 */ bf = Uint8Array.from(data.split(''), x=>x.charCodeAt(0)); var blob = new Blob([bf],{type:'image/png'}), e = d.createEvent('MouseEvents'), a = d.createElement('a'); /* クリックイベントを作成 */ e.initMouseEvent('click', false, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); /* ダウンロードデータをリンク先に設定 */ a.href = URL.createObjectURL(blob); a.download = 'paint.png'; /* ダウンロードリンクのイベントを発火 */ a.dispatchEvent(e); }

やっている事

・.toDataURL() で描画データを画像の埋込URL表現に変換 ・.replace() で画像データ部のみを取出 ・data.split('') で画像データを一文字ずつの配列に ・Uint8Array.from([],x=>x.charCodeAt(0)) でバッファデータに変換 ・Blob([],{type:'image/png'}) でバッファデータをpng画像データに ・.createEvent('MouseEvents') でマウスイベントを作成 ・.createElement('a') でリンクオブジェクトを作成 ・.initMouseEvent('click') でクリックイベントを作成 ・a.href = URL.createObjectURL() でリンク先を設定 ・a.download = 'paint.png' でダウンロードファイル名を設定 ・a.dispatchEvent(e) でリンク先をクリックした状態に

以上で完成となります。

プログラム完成後のカスタマイズ

現在までのコードでは、ウィンドウサイズを変更したり、スマホの縦横を変更すると canvasのサイズも変更されて描画位置がズレることがあります。対策としては、 windowサイズが変更された時に、canvasのサイズも更新する方法があります。

対策済みの全ソースファイルはこちらより入手(購入)可能です。

今回は学習用として作成しているため、必要最小限の機能となっていますが、 下記のようなカスタマイズで、高機能化を目指せます。

カスタマイズ例

・作成した画像データをアップロード投稿できるように ・画像を読み込んで編集できるように ・四角形を描き込めるように ・円を描き込めるように ・複数の画像をスタンプ状に押せるように ・画像を拡大、縮小、回転、反転できるように