2016/01/26
Canvasの複数の図形をprototypeで同時に扱う
JavaScriptのprototypeを利用すると、
同時に同様の図形を処理するような場合に、
メモリを節約した、効率的な処理を行うことができます。
この処理は、HTML5のCanvasを利用する際には、とても重要になりますので、
今回、prototypeを利用して、Canvas上で複数の図形を扱うプログラムを作ってみましたので、
ご紹介いたします。
今回作ってみたものは、Canvas上のなにもないところをクリックしたときに、その場所にサイズと色をランダムに設定した円を描画し、
円をドラッグした場合には、円を移動するというものです。
↓作ってみたもの
方法
全体の処理
- Canvasの初期設定
- クリックした場所の取得
- クリックした場所に円があるかを判定 (prototypeのインスタンスのメソッドを利用)
- 円がなければクリックした場所に円を追加 (prototypeのインスタンスのメソッドを利用)
- 円があれば、円を移動 (prototypeのインスタンスのメソッドを利用)
prototypeのメソッド
- 円の描画 (Canvasに変化(円の追加・移動)がある度に実行)
- クリックした場所に円があるかを判定 (クリック時に実行)
円の描画とクリックしたときの円の当たり判定は、
同じ処理を繰り返し同時に処理を行う必要があるため、
prototypeとして用意しておき、
その他の全体に関わる処理や、インスタンスの操作はprototypeの外で処理するようにしました。
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 | //円のコンストラクタを設定 function $Circle($ctx, x, y, r, color) { this.$ctx = $ctx; //Canvasのコンテキスト this.x = x || 0; //円の横位置(X) this.y = y || 0; //円の縦位置(Y) this.r = r || 0; //円の半径 this.color = color || "rgb(0, 0, 0)"; //円の色 } //円のメソッドをprototypeで設定 $Circle.prototype = { //円の描画 draw: function() { $ctx = this.$ctx; $ctx.beginPath(); $ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); $ctx.fillStyle = this.color; $ctx.fill(); $ctx.closePath(); }, //円の当たり判定 isTouched: function(cx, cy) { return ((this.x - cx) * (this.x - cx) + (this.y - cy) * (this.y - cy) <= this.r * this.r); } } |
Canvasのコンテキスト、円の座標、半径、色をコンストラクタとして設定し、
Cavansへのそれぞれの円の描画の処理と、それぞれの円がクリックされたかどうかの判定をprototypeメソッドで設定しました。
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 | //初期設定 var $canvasElm = document.getElementById("canvas"), //Canvasの要素を取得 $ctx = $canvasElm.getContext("2d"), //Canvasのcontextを取得 $circles = [], //円のインスタンスを入れておく配列を初期化 circleId = 0; //円のインスタンスのIDを初期化 /*-- マウス操作 --*/ //タッチデバイスとマウスデバイスのイベント振り分け var EVENT = {}; if ('ontouchstart' in window) { EVENT.TOUCH_START = 'touchstart'; EVENT.TOUCH_MOVE = 'touchmove'; EVENT.TOUCH_END = 'touchend'; } else { EVENT.TOUCH_START = 'mousedown'; EVENT.TOUCH_MOVE = 'mousemove'; EVENT.TOUCH_END = 'mouseup'; } //クリック時 or ドラッグ開始時の動作 $canvasElm.addEventListener(EVENT.TOUCH_START, function(e){ e.preventDefault ? e.preventDefault() : e.returnValue = false; //デフォルトのイベント効果を無効化 var scale = $canvasElm.width / $canvasElm.clientWidth, //レスポンシブでのCanvasの縮小率を取得 cx = e.pageX ? (e.pageX - $canvasElm.offsetLeft) * scale : (e.changedTouches[0].pageX - $canvasElm.offsetLeft) * scale, //Canvas上のマウスの横位置(X)を取得 cy = e.pageY ? (e.pageY - $canvasElm.offsetTop) * scale : (e.changedTouches[0].pageY - $canvasElm.offsetTop) * scale, //Canvas上のマウスの縦位置(Y)を取得 isCircleTouched = false; //円がタッチされたときのフラグの初期化 $ctx.clearRect(0, 0, $canvasElm.width, $canvasElm.height); //Canvasをクリアする for(var i = 0; i < $circles.length; i++) { //描画済みの円についてそれぞれ処理 if($circles[i].isTouched(cx, cy)) { //円がタッチされている場合 $canvasElm.data = { prevX: $circles[i].x, //移動前の円の横位置 prevY: $circles[i].y, //移動前の円の縦位置 prevCX: cx, //移動前のマウスの横位置 prevCY: cy, //移動前のマウスの縦位置 touchedId: i //移動対象の円のインスタンスID } isCircleTouched = true; //円がタッチされていたらフラグをtureに } } if(isCircleTouched) { //一つでも円がタッチされていたら、移動を有効にする $canvasElm.data.down = true; } else { //円が1つもタッチされていない場合は円のインスタンスを追加 $circles[circleId] = new $Circle($ctx, cx, cy, Math.random() * (30 - 10) + 10, "rgb(" + Math.round(Math.random() * (191 - 146) + 146) + ", " + Math.round(Math.random() * (219 - 204) + 204) + ", " + Math.round(Math.random() * (239 - 140) + 140) + ")"); //インスタンスを追加(半径と色をランダムに設定) circleId++; //円のインスタンスIDを更新 } //円のインスタンスをそれぞれ描画 for(var i = 0; i < $circles.length; i++) { $circles[i].draw(); } }); //ドラッグ中の操作 $canvasElm.addEventListener(EVENT.TOUCH_MOVE, function(e){ e.preventDefault ? e.preventDefault() : e.returnValue = false; var scale = $canvasElm.width / $canvasElm.clientWidth, cx = e.pageX ? (e.pageX - $canvasElm.offsetLeft) * scale : (e.changedTouches[0].pageX - $canvasElm.offsetLeft) * scale, cy = e.pageY ? (e.pageY - $canvasElm.offsetTop) * scale : (e.changedTouches[0].pageY - $canvasElm.offsetTop) * scale; if($canvasElm.data) { if($canvasElm.data.down) { //移動が有効になっていたら $ctx.clearRect(0, 0, $canvasElm.width, $canvasElm.height); //Canvasをクリア $circles[$canvasElm.data.touchedId].x = $canvasElm.data.prevX + (cx - $canvasElm.data.prevCX); //円をドラッグした分だけ移動(X) $circles[$canvasElm.data.touchedId].y = $canvasElm.data.prevY + (cy - $canvasElm.data.prevCY); //円をドラッグした分だけ移動(Y) } } for(var i = 0; i < $circles.length; i++) { $circles[i].draw(); } }); //ドラッグ終了時の操作 $canvasElm.addEventListener(EVENT.TOUCH_END, function(e){ e.preventDefault ? e.preventDefault() : e.returnValue = false; if($canvasElm.data) { if($canvasElm.data.down) { //移動が有効になっていたら var scale = $canvasElm.width / $canvasElm.clientWidth, cx = e.pageX ? (e.pageX - $canvasElm.offsetLeft) * scale : (e.changedTouches[0].pageX - $canvasElm.offsetLeft) * scale, cy = e.pageY ? (e.pageY - $canvasElm.offsetTop) * scale : (e.changedTouches[0].pageY - $canvasElm.offsetTop) * scale; $ctx.clearRect(0, 0, $canvasElm.width, $canvasElm.height); $circles[$canvasElm.data.touchedId].x = $canvasElm.data.prevX + (cx - $canvasElm.data.prevCX); $circles[$canvasElm.data.touchedId].y = $canvasElm.data.prevY + (cy - $canvasElm.data.prevCY); for(var i = 0; i < $circles.length; i++) { $circles[i].draw(); } $canvasElm.data = null; //移動を無効に } } }); |
円のインスタンスは配列にまとめて追加していくようにして、
配列に追加されたインスタンスを全て処理するという仕組みにしました。
ドラッグ時の円の移動は、必ず1つずつ行うので、インスタンスの外で処理しています。
ドラッグの処理で、ドラッグ前の状態や、ドラッグ中であることを保存しておく必要があるので、
Canvasの要素にdataというプロパティを追加して、処理を補助するようにしました。
参考図書
ブレイクスルーJavaScript フロントエンドエンジニアとして越えるべき5つの壁―オブジェクト指向からシングルページアプリケーションまで
参考サイト
タッチデバイスとマウスデバイスのイベント振り分け
[JS] タップイベントが実装されているのかを調べる方法(2つ)- YoheiM .NET
Author Profile
NINOMIYA
Webデザイナー兼コーダー出身のフロントエンド開発者です。 UXデザインやチーム開発の効率化など、勉強中です。
SHARE