このサイトでは、分析、カスタマイズされたコンテンツ、および広告に Cookie を使用します。このサイトを引き続き閲覧すると、Cookie の使用に同意するものと見なされます。
Hi, Developers,
straightapps.com ロゴ
作成 October 24, 2020
トップページ > Web 開発トップ > マルチタッチを検出&処理
line
Web 開発
line

Canvas のマルチタッチを検出し、処理します。

タッチデバイスでマルチタッチを検出し、処理できるようにしたテストプログラムです。 PC + マウスのような、非タッチデバイスでは動作しません。 コード中心ですので、動作としては特に目的はありません。

※ 表示が正しくないと思う場合は、JavaScript を有効にしてください。

このプログラムは、タッチデバイス版 Firefox ブラウザでは、縦方向にページがスクロールしてしまい、操作しにくい可能性があります。 ブラウザの特性によるものですので、Chrome など別なブラウザでの操作をお勧めします。

Your browser does not support canvas element.


タッチデバイスでマルチタッチを検出します。

スマホやタブレットなど、タッチ可能なデバイスでマルチタップを検出します。 タップ受付可能なブラウザが必要ですので、PC 版のブラウザでは動作しません。 また、マルチタップの1タップ検出とマウスのクリックは異なりますので、 マウスのクリックで1タップをシミュレートすることはできません。

ウェブページを開くと、canvas の領域がブラウザと同じサイズで作成され、グレーで塗りつぶされます。

指1本でタッチすると、 canvas の背景が一瞬薄い緑になったあとピンクに変わり、左上に情報が表示されます。 タッチした位置には、黒い四角が表示されます。 タッチしたまま移動すると、黒い四角も追従してきて、 指を離すと薄紫の背景に変わります。 なお、動作について、詳しくはそれぞれのセクションに書いています。

指2本でタッチすると、 先にタッチした位置には赤い丸が、次にタッチした位置には青い丸が描画され、その間が黒いラインでつながれます。 そのまま移動すると、追従してきます。

動作としてはそれだけですが、コードとしてはタッチの検出や canvas に対する座標の変換が含まれています。 これを理解すれば、タッチによるスクロール、ピンチによる拡大縮小、2本指での回転などが実現できそうです。


現時点でのテスト済みブラウザは、

  • Android 9 スマホの Chrome, FireFox, Edge
  • です。

    最初にも書いていますが、Firefox では canvas 内のタッチでもページがスクロールしてしまいます。 これはブラウザの問題(仕様)なので、あきらめるか、他のブラウザを使ってください。

    スマホやタブレットで開く場合は、 この QR コード ( URL の QR コード
    https://www.straightapps.com/web/js-multi-touch.html
    )
    が、このページの URL になっています。 QR コード読み取りアプリで読み取ると便利です。

    ▲ページ先頭へ

    ここから先は開発情報です

    投稿 October 23, 2020

    ここから先は、上記、マルチタップ検出プログラム開発を行った際に調べたりした JavaScript コードについての情報です。

    作者背景については、「素因数分解トレーニング」のページの 「ここから先は開発情報です」をご覧ください。

    まずは、HTML 部です。

    <canvas id="canvas-main">Your browser does not support canvas element.</canvas>
    
    <script type="text/javascript" src="js/multi-touch.js"></script>
    

    特別なところはなく、ただ canvas を定義しているだけです。 今回新しく、canvas をサポートしていないブラウザ向けに、メッセージが表示されるようになっています。 ただ、そのようなブラウザが手元にありませんので、動作確認はできていません。

    以降のセクションで、コード詳細を確認していきます。

    なお、ご参考までに、JavaScript ソースコードそのものを、拡張子 js を拡張子 txt に変えて、次のリンクより開けるようにしてあります。
    multi-touch.txt
    ※ 作成時はタブを4文字としていますので、環境によってはタブが8文字のため、 コメント等がずれて見えるかも知れません。
    ※ 念のため書き添えますが、このソースコードをこのまま転載・公開することはご遠慮ください。

    ▼ セクション一覧

    JavaScript コードのロード
    touchstart イベント処理
    touchend イベント処理
    touchcancel イベント処理
    touchmove イベント処理
    touchmove イベント処理(複数タッチ)
    動作させてみた感想

    なお、本サイトのご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。

    ▲ページ先頭へ

    JavaScript コードのロード

    投稿 October 23, 2020

    JavaScript コード multi-touch.js は、 canvas を定義したすぐあと、 body タグ内でロードされています。 これは、HTML の body を先頭から読み込んでいき、 そのコードに到達したときに読み込まれる、というものです。

    これより前で定義された canvas id="canvas-main" にアクセスできます。

    なお、JavaScript のコードは、head や body タグ内に直接記述することも可能です。 機能テストには有効かもしれませんが、ごちゃごちゃになるので、おすすめできる方法ではありません。 また、HTML タグの onClick などに直接コードを書くこともできるようですが、小規模でない限りは、混乱のもとになるかと思います。

    <script type="text/javascript">
    <!--
    	<!-- JavaScript コードを記述 -->
    // -->
    </script>
    <noscript>
    	<!-- JavaScript が無効の場合の注意書きなどを記述 -->
    </noscript>
    

    ロードされると、js ファイルの先頭から、 関数でない部分が順次、その場で実行されます。 multi-touch.js で、ロード時に実行されるコードは、次の通りです。

    // 画面サイズ・ブラウザサイズを取得
    var scr = new Object();					// 画面のサイズ情報を保持するオブジェクト
    scr.width = screen.width;				// 画面のサイズ
    scr.innerWidth = window.innerWidth;			// ウィンドウ内側の幅
    scr.innerHeight = window.innerHeight;			// ウィンドウ内側の高さ
    scr.clientWidth = document.documentElement.clientWidth;	// 基本的にはinnerHeightと同じ?
    

    canvas のサイズを決めるため、画面やブラウザのサイズを取得しています。 小さすぎれば処理を確認しにくくなりますし、大きすぎれば無駄になっていまいますので、 利用可能なサイズいっぱいまでに設定するためです。

    オブジェクト scr は、C/C++ で言う構造体のようなものですが、 事前にメンバー変数を定義することなく、自由に使えてしまうというものです。 オブジェクトについては、 「【基礎】Object 型の変数の使い方」に詳しく書いています。

    // canvasの設定
    var canvas = document.getElementById('canvas-main');
    canvas.width = scr.innerWidth;				//canvasの横幅
    canvas.height = scr.innerHeight;			//canvasの縦幅
    
    // コンテキストを取得
    var ctx = canvas.getContext('2d');
    

    HTML の canvas id="canvas-main" 要素を canvas に取得し、 その幅と高さを設定してサイズを決めます。 そのあと、2D 描画を実行するため、2D コンテキストを取得しています。

    // 初期の色で塗りつぶし
    ctx.fillStyle = "rgb(192,192,192)";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    

    動作を確認しやすくするため、canvas の背景色をグレーで塗りつぶしておきます。

    これで初期化は終了ですので、タッチの監視を実行します。

    function startup()
    {
    	canvas.addEventListener("touchstart", TouchStart, false);	// 1タッチ目が開始された
    	canvas.addEventListener("touchend", TouchEnd, false);		// タッチが終了した
    	canvas.addEventListener("touchcancel", TouchCancel, false);	// 場外(ブラウザのコントロール領域など)に出た
    	canvas.addEventListener("touchmove", TouchMove, false);	// タッチしたまま移動した
    }
    
    if (document.readyState === 'loading') {
    	document.addEventListener("DOMContentLoaded", startup);
    }
    else{
    	startup();
    }
    

    タッチイベントの監視開始部分です。

    処理の流れとしては、いったん startup 関数は読み飛ばされ、 次の if 文に入ります。

    HTML ドキュメント、すなわちページを読み込み中の場合は、 DOMContentLoaded イベントが発生したら、 startup 関数を実行するように指示しています。 DOMContentLoaded イベントは、 HTML ドキュメントが完全に読み込まれ解釈された時点で発生するとされていて、 スタイルシート、画像、サブフレームの読み込みが完了するのを待ちません。 このため load イベントと異なり、 JavaScript の解釈がここに来る前にイベントが発生してしまうと、 このあといくら待ってもイベントが発生しないため、 ロード中でないときは、直接 startup 関数を実行するようにしています。

    startup 関数では、 touchstarttouchendtouchcanceltouchmove イベントのハンドラを定義しています。 それぞれのイベントおよびハンドラについては、以下で詳しく触れています。

    ▲ページ先頭へ

    touchstart イベント処理

    投稿 October 23, 2020

    タッチされていない状態からタッチを検出したとき、このイベントが発生します。

    function TouchStart(event)
    {
    	event.preventDefault();					// デフォルトの処理を禁止
    
    	ctx.fillStyle = "rgb(220,255,220)";
    	ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
    

    touchstart イベントが発生すると、この関数が呼び出されます。

    touchmove イベントと同じ方法では、タッチされた座標を取得できないようですので、 ここではこの関数を通ったことを明らかにするため、 薄い緑で canvas 背景を塗りつぶすだけにしています。

    その前にある preventDefault 関数呼び出しは、 ブラウザのデフォルトの処理を行わないよう指示している関数ですが、 ここではあってもなくても変わらないかも知れません。

    ▲ページ先頭へ

    touchend イベント処理

    投稿 October 23, 2020

    タッチされている状態からタッチがなくなったとき、このイベントが発生します。

    function TouchEnd(event)
    {
    	event.preventDefault();
    
    	ctx.fillStyle = "rgb(220,220,255)";
    	ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
    

    touchend イベントが発生すると、この関数が呼び出されます。 画面から指を離したときにこのイベントが発生しますが、 イベントが発生するのはタッチがなくなったときのみです。 2本指でタッチしていて1本を離しても、タッチはなくなっていないので、このイベントは発生しません。

    ここではこの関数を通ったことを明らかにするため、 薄い青で canvas 背景を塗りつぶすだけにしています。

    その前にある preventDefault 関数呼び出しは、 ブラウザのデフォルトの処理を行わないよう指示している関数ですが、 ここではあってもなくても変わらないかも知れません。

    ▲ページ先頭へ

    touchcancel イベント処理

    投稿 October 23, 2020

    タッチされている状態からタッチ検出不能になったとき、このイベントが発生します。

    function TouchCancel(event)
    {
    	event.preventDefault();
    
    	ctx.fillStyle = "rgb(255,0,0)";
    	ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
    

    touchcancel イベントが発生すると、この関数が呼び出されます。 説明としては、ブラウザや画面でタッチ検出領域外のところ、 例えばツールバー部分やナビゲーションバー部分に出るとこのイベントが発生する、 とされているようなのですが、発生させることができませんでした。

    唯一発生させることができた手順は、画面にタッチしたまま電源ボタンを押してロック状態にします。 そこから指紋などでログインしなおすと、このイベントが発生しました。

    ここではこの関数を通ったことを明らかにするため、 赤で canvas 背景を塗りつぶすだけにしています。

    その前にある preventDefault 関数呼び出しは、 ブラウザのデフォルトの処理を行わないよう指示している関数ですが、 ここではあってもなくても変わらないかも知れません。

    ▲ページ先頭へ

    touchmove イベント処理

    投稿 October 23, 2020

    タッチされている状態からタッチが移動したとき、このイベントが発生します。 このプログラムのメイン部分です。

    function TouchMove(event)
    {
    /*
    	if (event.cancelable){
    		str = "キャンセル可能";
    	}
    */
    	event.preventDefault();
    /*
    	if (event.defaultPrevented){
    		str += " 禁止された";
    	}
    */
    
    	//	Firefox では、「キャンセル可能」かつ「禁止された」になりますが、preventDefault は動作しません。
    	//	このため、canvas 内で操作していても、ページがスクロールしてしまいます。
    

    まずは、デフォルトの処理の禁止処理です。 タッチ&移動では、通常、スクロール処理が行われます。 canvas 上で操作をしている場合、スクロールしてしまうと操作がしにくくなりますので、 canvas 上での操作のみ、スクロールを行わないよう指示しています。

    event.preventDefault 関数で、デフォルトの処理を禁止しています。 私は Android の Firefox ブラウザで実機テストをしていましたが、 いくら preventDefault 関数を呼んでもスクロールは停止できませんでした

    event.cancelable プロパティを参照すると、 発生したプロパティがキャンセル可能なのかどうかを知ることができます。 Firefox では、キャンセル可能でした。

    event.defaultPrevented プロパティは、デフォルトの処理が禁止されたかどうかを保持します。 直前で preventDefault を呼んでいますので、効いているなら true になります。 Firefox でも、ちゃんと true になっていました。

    かなり悩みましたが、結論としては、コードの問題ではなく、Firefox がそういう仕様になっている、ということでした。 実際に、Chrome や Edge で実行させてみると、canvas 内での操作により、スクロールすることはありませんでした。

    	// canvas の絶対座標位置を取得します。
    	//    ※ スクロール状態に関係なくページに対する座標。
    	var rect = event.target.getBoundingClientRect();
    

    このあと取得するタッチ位置を示す座標は、 スクロールに関係なくページの原点からの座標か、 画面に対する座標でした。 使いたいのは canvas の左上からの座標ですので、 共通で使用する「canvas の描画位置」を取得しておきます。 なお、rect に設定されるのは、なぜか整数ではなく実数です。

    	// canvas 内に1タッチだけある場合
    	if (event.targetTouches.length == 1) {
    
    		// 動きがわかりやすいように、ピンクでクリア
    		ctx.fillStyle = "rgb(255,220,220)";
    		ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    		var touch = event.targetTouches[0];			// タッチ1の座標
    		let x = Math.floor( touch.clientX - rect.left);	// 現在のスクロール位置に対する座標
    		let y = Math.floor( touch.clientY - rect.top);
    		//let x = Math.floor( touch.pageX - rect.left);		// スクロール状態に関係なくページに対する座標
    		//let y = Math.floor( touch.pageY - rect.top);
    

    タッチが1つだけのときの処理です。 引数で渡された event の targetTouchesタッチ情報を保持する配列であり、 その length プロパティを見れば、 それが 1 のときは1タッチのみであると判断できます。 配列について、詳しくは「【基礎】可変サイズの配列と操作」に書いています。

    まずは、処理状態がわかりやすいように、ピンクで背景を塗りつぶしています。 画面にタッチすると touchstart イベントが発生して背景が薄い緑になり、 そのあと指を動かすと、この touchmove イベントが発生して、 背景がピンクに変化します。

    変数 touch にタッチ1の情報を取得します。 targetTouches 配列のインデックス 0 が、タッチ1の情報です。

    続く x と y の設定で、canvas に対する座標を設定しようとしています。

    touch.clientX、touch.clientY には、ブラウザのクライアント領域に対する座標が設定されていました。 rect に canvas 自身の描画位置(左上の座標)を取得済みですので、これをマイナスすれば、canvas に対する座標になります。 rect が実数で返っていますので、Math.floor 関数を使用して、整数化しています。

    ちなみにネット上でよく見かけた touch.pageX、touch.pageY には、スクロール状態によらず、 ページの左上からの座標が設定されるようです。 たくさんスクロールしたあとに取得されれば、touch.pageY が画面サイズを超えた大きな値になる、という具合です。 どういうケースで役に立つのかは、今はわかりません。

    		ctx.font = "16px 'ヒラギノ角ゴ Pro', 'Hiragino Kaku Gothic Pro', 'MS ゴシック', 'MS Gothic', sans-serif";
    		ctx.fillStyle = "#000";
    		ctx.fillText( touch.pageY, 10, 20 );			// クライアント領域の座標(y座標は'下'を指している)
    		ctx.fillText( touch.clientY, 10, 40 );			// クライアント領域の座標(y座標は'下'を指している)
    		ctx.fillText( rect.top, 10, 60 );
    
    		ctx.fillStyle = "rgb(0,0,0)";
    		ctx.fillRect( x - 25, y - 25, 50, 50 );
    	}
    

    1タッチの処理の続きで、情報の表示部分です。 フォントを用意して、canvas の左上に情報を出力しています。

    1行目は touch.pageY を、(10, 20) の位置に出力しています。 コメントにもあるように、y 座標である 20 は、テキストの下のラインになるようです。 値としては、スクロールの影響を受けない、ページトップからの y 座標と言えます。

    2行目は touch.clientY で、ブラウザの クライアント領域 ( 上部タイトルやツールバーを含まない、コンテンツを描画できる領域のことです。 ) に対する座標です。 ページのスクロール位置によらず、同じ位置のタッチなら、同じ値が返るようです。 画面を上下に分割した場合、下のウィンドウになっているときにも、 スクリーンに対する座標ではなく、ちゃんとウィンドウ(ブラウザ)に対する座標になっていましたので、 そのあたりを気にする必要はありません。

    3行目canvas 自身の y 座標です。 2行目にある値からこの3行目にある値を引いたものが、canvas に対するタッチの座標と言えます。

    座標計算が正しいことを確認するため、最後にタッチ座標を中心に、黒い四角を描画しています。

    1タッチ時の画面イメージ

    ▲ページ先頭へ

    touchmove イベント処理(複数タッチ)

    投稿 October 24, 2020

    タッチされている状態からタッチが移動したとき発生する、 touchmove イベントを処理するこのプログラムのメイン部分、TouchMove 関数の続きです。

    	else if (event.targetTouches.length == 2) {
    
    		// 動きがわかりやすいように、グリーンでクリア
    		ctx.fillStyle = "rgb(128,255,128)";
    		ctx.fillRect(0, 0, canvas.width, canvas.height);
    

    タッチの情報は、引数 event の targetTouches 配列に入りますので、 length プロパティを調べれば、今いくつのタッチがあるかを知ることができます。 ここでは、2タッチの処理を行うこととしています。 3タッチになれば、この条件にはあわなくなります。

    まずは、処理状態がわかりやすいように、薄い緑で背景を塗りつぶしています。 画面にタッチすると touchstart イベントが発生して背景が薄い緑になり、 そのあと指を動かすと、この touchmove イベントが発生して、 背景が薄い緑に変化します。 1タッチの時はピンクですので、その違いもわかります。

    		// 描画準備
    		ctx.lineWidth = 1;
    		var sx = 0, sy = 0;					// 1点目の座標
    		var x, y;
    
    		for (var i = 0; i < event.touches.length; i++) {
    			var touch = event.touches[i];
    			x = Math.floor( touch.clientX - rect.left);	// 現在のスクロール位置に対する座標
    			y = Math.floor( touch.clientY - rect.top);
    
    			ctx.fillStyle = "rgb(0,0,255)";
    			if (i == 0){					// 1点目の座標を保存
    				sx = x;
    				sy = y;
    				ctx.fillStyle = "rgb(255,0,0)";
    			}
    
    			ctx.beginPath();
    			ctx.arc( x, y, 20, 0, 2 * Math.PI, true );	// 座標, 半径, 開始ラジアン角, 終了ラジアン角, 時計回りに描画
    			ctx.fill();
    			ctx.stroke();
    		}
    

    コンテキストの lineWidth プロパティは、描画する線の太さを指定するものです。 ここでの設定は、このあと for 内にある arc 関数、 すなわち円の描画のふちの太さになっています。 このあと出てきますが、直線を引く場合は、その線の太さになります。

    変数 sx, sy はタッチ1の座標を保持するため、 変数 x, y はタッチ2の座標を保持するための用意です。

    for ループで、タッチされた点を1つずつチェックします。 この場合、if 文で2点だけの処理になっていますので、必ず2回です。

    いったん、変数 touch に対象のタッチ情報を取り出しています。 そこから canvas に対する座標を変数 x、y に設定しています。 タッチが1つのときと全く同じです。

    いったん塗りつぶし色を青に設定していますが、 タッチ1の場合は、座標を sx、sy にコピーしておき、塗りつぶし色を赤に変更しています。 つまり、タッチ1は赤タッチ2は青で、 円が描画されることになります。

    2タッチ時の画面イメージ

    beginPath 関数は、「パス」を開始するためのものだそうです。 ここでは、円を指定し、中を塗り、描画する、という一連の動作の開始、という意味でいいのではないかと思っています。 これがない場合は、それぞれの描画コマンドが単発動作となって、思った通りにならない、のではないでしょうか。

    arc 関数は、円を描くための関数です。 中心座標のあと、半径、開始角、終了角、描画の向きを指定できます。 角度は「ラジアン」を使うということです。 0 〜 360 度を、0 〜 2πで表しただけですので、簡単に変換可能です。

    fill 関数で、円の中身を塗りつぶしています。 この色が、fillStyle で指定した色になっています。 最後に stroke 関数は、ふちを描画しているようです。 黒で1ピクセルですが、この太さが lineWidth プロパティで設定したものになっていました。 ふちの色は指定できるのか指定できないのか、わかりません。

    		ctx.lineWidth = 10;
    		ctx.lineCap = "round";		// butt、square も使える
    
    		ctx.beginPath();
    		ctx.moveTo( sx, sy );
    		ctx.lineTo( x, y );
    		//closePath();	lineTo を2回以上使用して始点まで線を引きたいとき
    		ctx.stroke();
    	}
    }
    

    将来回転の操作を実装するためにも、タッチの角度の計算ができるようにしておこうと思いました。

    変数 sx, sy にタッチ1の座標が、 変数 x, y にタッチ2の座標が入っていますので、 その間を線でつないでいます。

    lineWidth は線の太さです。 lineCap は線のスタイルです。 ここで指定している round は、両端を丸める指定だそうです。 このほか、butt では両端を最短サイズに、 square では両端を太さの半分はみださせた指定だそうです。

    beginPath 関数で描画を開始し、 moveTo 関数で始点の位置を指定、 lineTo 関数でここまで線を引きます。 実際には stroke 関数が呼び出されたときに描画されるようです。

    コメントでメモしてありますが、moveTo で始点に移動、 lineTo で線を引き、さらに lineTo すると先の lineTo の終点から連続するラインが引かれます。 これをどんどんつなげて多角形を描けると思いますが、 moveTo した始点に戻して終了したいとき、closePath 関数を呼べばいいようです。 「図形が閉じている」と判断されるための条件になるようなら、 これを呼ばずに lineTo で始点に戻しても、fill 関数による塗りつぶしはできないと想像できます。

    ▲ページ先頭へ

    動作させてみた感想

    投稿 October 24, 2020

    1タッチ、2タッチとも、だいた思うとおりに動作していますので、 デバイスが受け入れる限りは3タッチ以上も同様に処理できると思います。

    気になる動作としては、 1タッチで背景がピンクになり、離すと青っぽくなるのは想定通りですが、 1タッチ状態で2タッチ目をすると、 一瞬だけ薄い緑の背景になるので、 touchstart イベントが発生していると言えそうです。

    また、2本タッチすると、先にタッチしたほうに赤、あとからのほうに青で円が描画されます。 あとにタッチしたほうを離し、再びタッチすることでまた青の円が描画されますが、 2本タッチ中に「先にタッチしたほう」を離し、 再びタッチすると赤の円が描かれます。

    仮定と touches 配列のインデックスで上記を書き直します。

    (1)人差し指をタッチ、 (2)中指をタッチ、すると人差し指に赤、中指に青が描画されます。 (3)中指だけを離し、また中指をタッチすると中指に青が描画されます。 これは普通であり特に何でもありません。

    この、人差し指が touches[0]、中指が touches[1] の状態で、 (4)人差し指だけを離し、また人差し指をタッチすると、 人差し指が赤、中指が青で描画されます。

    中指だけになったとき、touches[0] で黒い四角が描画されていますが、 再び人差し指がタッチされると、 人差し指が touches[0]、中指が touches[1] になっています。 touches の情報にタッチ ID が入っている、ということでしょうから、 場合によっては配列インデックスだけで処理すると問題があるケースがあるかも知れません。 どういうケースで注意すべきかは思いつきませんし、 タッチ ID が取り出せるのかは、今はわかりませんが。

    この先は、body に canvas だけを持つ html を作成して、 すべての処理を canvas だけで行うことができるなら、 いろいろなことができそうですね。

    ▲ページ先頭へ
    line
    関連トピックス
    line


    その他のおすすめ

    ウェブ開発に関するトピックは、「ウェブ開発トップ」にまとめられています。



    © 2017-2021 StraightApps.com 無断転載を禁じます。No reproduction without permission.