こんにちは、中山です。
ウェブページ上で、マウスやタッチ操作で自由に描画できるような仕組みを実装したい場合、「 <canvas> 」タグを用いれば、比較的簡単に実現することができます。
しかし、 「Windows8」+「IE10」 という組み合わせの環境で動作させようとする場合は、実は少し特別な仕組みが必要となるのです。
経緯
私は以前、iPhone、iPadやAndroid端末向けのウェブページの開発も少し手掛けていたことがありました。
そのときの経験から、当初は、以下のように考えていました。
Windows8ではタッチパネルからの操作も行えるんだな・・・。
タッチ操作を検知するには、iOSやAndroidと同じように、Javascriptの「touchstart」「touchmove」などのイベントをハンドリングすればいいだろう・・・。
しかしこのように安易に考えていたのがいけなかった。
「Windows8」+「IE10」という環境では、「touchstart」や「touchmove」イベントをハンドリングすることはできなかったのでした。
一瞬くじけそうになりながらも、いやいやこのままではダメだと立ち上がり、 「Windows8」+「IE10」という環境で正しく動作する、「Canvasお絵描きツール」 を実現するために、少し調べてみることにしたのでした。
実装例
以下のような「お絵描きツール」を作ります。(IE10ブラウザで表示したものです。)
(※実際に動作するコードのサンプルは、この記事の末尾に掲載します。)
実装のポイント
ポイントとなるのは、以下の2点です。
1.MSPointerDown、MSPointerMove、MSPointerUp イベントのハンドリング
iOSやAndroidのタブレット、スマートフォンのブラウザの場合、そこに表示されるウェブページ上のタッチ操作をハンドリングするために、
- touchstart
- touchmove
- touchend
といったJavascriptのイベントを利用することができます。
また、PCの一般的なブラウザでは、ウェブページ上のマウスの軌跡をハンドリングするために、
- mousedown
- mousemove
- mouseup
といったJavascriptイベントが用意されています。
しかし、「Windows8+IE10」環境では、これらではなく、
- MSPointerDown
- MSPointerMove
- MSPointerUp
という、IE10独自のイベントを使うことができるようです。
また、これらのイベントが利用可能かどうかを判定するために、
window.navigator.msPointerEnabled
という、これも独自のプロパティが用意されているようです。
このプロパティは、例えば以下のように利用します。
var touchstart= 'touchstart'; if (window.navigator.msPointerEnabled) { // msPointerEnabledプロパティの値がtrue(=Windows8+IE10)であれば、 // イベントハンドラ名を「MSPointerDown」に設定する touchstart= 'MSPointerDown'; } this.canvas.addEventListener(touchstart, function(event) { // Do something }, false);
「MSPointerDown」「MSPointerMove」「MSPointerUp」イベントを使う意義
実は、「Windows8+IE10」環境においては、従来の
- mousedown
- mousemove
- mouseup
イベントを用いることで、マウス操作だけではなく、「タッチ」操作も、とりあえずハンドリングすることができるのです。
それでは、わざわざ「MSPointerDown」「MSPointerMove」「MSPointerUp」イベントを使う必要はないのではないか、と思われるかもしれません。
しかしそうではありません。これら「MSPointer~」イベントを用いることで、
といったことが、簡単に実現できるのです。
より柔軟にタッチ操作を取り扱うためには、これらの「MSPointer~」イベントを積極的に利用していくのがよいのでしょう。
(とは言え、今回のサンプルコードでは、ごく基本的な処理しか行っていないのですけれども。)
2."-ms-touch-action:none;"
タッチスクリーンを搭載している端末で、このお絵描きツールを使おうとした場合、1点問題が発生します。
それは、キャンバス領域内で、スクリーン上をドラッグして線を描画しようとしても、そのドラッグが、画面のスクロールの操作だと認識されてしまい、正しく描画ができない、というものです。
これを防ぐために、「 <canvas> 」タグに、以下のように「 -ms-touch-action:none 」スタイルシート属性を指定します。
<canvas id="myCanvas" style="-ms-touch-action:none;"></canvas>
これも、IE10が独自に拡張した属性のようです。
「-ms-touch-action」に「 none 」を指定することで、その「<canvas>」タグ上でドラッグしてもスクロール操作だと認識されず、正しく「<canvas>」タグ側でタッチイベントをハンドリングすることができるようになります。
一方、スクロール操作をそのまま認識させたい、という場合は、「-ms-touch-action」に「 auto 」を指定するとよいでしょう。
今回の内容は、以下の記事を参考にしました。
IE 10 および Metro スタイル アプリケーションでのタッチ入力
ただ実際のところ、こういったMicrosoft独自の仕様というのは、どちらかというと敬遠されがち、という部分もあると思います。
しかし、こうして実際に使ってみることで、ひょっとしたら、「意外と便利に使えるんじゃないか」という発見があるのかもしれませんね。
(参考)サンプルコード
実際に動作するコードのサンプルです。
IE10のほかにも、Firefox22.0、Chrome27、IE9や、iPad(iOS6.1)のSafariブラウザ、Android(OS4.2エミュレータ)のChromeブラウザにて動作を確認しています。
描画する線が少しギザギザだったり、ドラッグしたまま <canvas> 外に移動したときの制御ができていないのは、ご容赦ください。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="text/javascript"> var Draw = { canvas : null, context : null, pencil : null, eraser : null, drawing : false, startX : 0, startY : 0, offset : 5, initialize : function() { this.canvas = document.getElementById("myCanvas"); this.context = this.canvas.getContext("2d"); this.pencil = document.getElementById("pencil"); this.eraser = document.getElementById("eraser"); this.pencil.addEventListener("click", function(event) { Draw.setPencil(); // change edit style to "pencil". }, false); this.eraser.addEventListener("click", function(event) { Draw.setEraser(); // change edit style to "eraser". }, false); var touchstart = 'touchstart'; var touchmove = 'touchmove'; var touchend = 'touchend'; if (window.navigator.msPointerEnabled) { // for Windows8 + IE10 touchstart = 'MSPointerDown'; touchmove = 'MSPointerMove'; touchend = 'MSPointerUp'; } else if (document.ontouchstart === undefined) { // for other PC browsers touchstart = 'mousedown'; touchmove = 'mousemove'; touchend = 'mouseup'; } this.canvas.addEventListener(touchstart, function(event) { Draw.touchStart(event); // start drawing. }, false); this.canvas.addEventListener(touchmove, function(event) { Draw.touchMove(event); // continue drawing while dragging the pointer. event.preventDefault(); }, false); this.canvas.addEventListener(touchend, function(event) { Draw.touchEnd(event); // finish drawing. }, false); this.setPencil(); }, setPencil : function() { this.context.globalCompositeOperation = "source-over"; this.context.strokeStyle = "#ff0000"; this.context.lineWidth = 4; this.context.lineCap = "round"; }, setEraser : function() { this.context.globalCompositeOperation = "destination-out"; this.context.strokeStyle = "#000000"; this.context.lineWidth = 20; this.context.lineCap = "round"; }, touchStart : function(event) { this.drawing = true; if (event.offsetX === undefined) { if (event.type == 'touchstart') { this.startX = event.changedTouches[0].pageX - this.canvas.offsetLeft - this.offset; // for Android } else { this.startX = event.pageX - this.canvas.offsetLeft - this.offset; } } else { this.startX = event.offsetX - this.offset; } if (event.offsetY === undefined) { if (event.type == 'touchstart') { this.startY = event.changedTouches[0].pageY - this.canvas.offsetTop - this.offset; // for Android } else { this.startY = event.pageY - this.canvas.offsetTop - this.offset; } } else { this.startY = event.offsetY - this.offset; } }, touchMove : function(event) { if (this.drawing) { var offsetX = 0; var offsetY = 0; if (event.offsetX === undefined) { if (event.type == 'touchmove') { offsetX = event.changedTouches[0].pageX - this.canvas.offsetLeft - this.offset; // for Android } else { offsetX = event.pageX - this.canvas.offsetLeft - this.offset; } } else { offsetX = event.offsetX - this.offset; } if (event.offsetY === undefined) { if (event.type == 'touchmove') { offsetY = event.changedTouches[0].pageY - this.canvas.offsetTop - this.offset; // for Android } else { offsetY = event.pageY - this.canvas.offsetTop - this.offset; } } else { offsetY = event.offsetY - this.offset; } this.context.beginPath(); this.context.moveTo(this.startX, this.startY); this.context.lineTo(offsetX, offsetY); this.context.stroke(); this.context.closePath(); this.startX = offsetX; this.startY = offsetY; } }, touchEnd : function(event) { this.drawing = false; } }; </script> </head> <body onload="Draw.initialize();"> <h1>canvas draw tool for Windows8 + IE10</h1> <div style="margin-left:20px; margin-top:20px; margin-right:20px; margin-bottom:12px;"> <div style="width:600px;border:2px solid #808080;"> <!-- disables scrolling by "-ms-touch-action:none" --> <canvas id="myCanvas" height="400" width="600" style="-ms-touch-action:none;"></canvas> </div> </div> <div style="margin-left:20px; margin-top:0px; margin-right:20px; margin-bottom:20px;"> <input type="button" id="pencil" value="ペン" /> <input type="button" id="eraser" value="消しゴム" /> </div> </body> </html>