14日目:クリックされたら数字を暗くする

14日目です。今回は数字がクリックされたら暗くする処理を追加します。また、プログラムで重複している部分を少しまとめます。例によって今回追加修正した箇所には●印を付けてあります。
div要素やinput要素を使って作成したバージョンはクリックした事を示すために背景色を赤色にしていました。オリジナルのTEN -10-では数字がタッチされると暗くなるようになっています。今回はクリックされたら数字を暗くする、というか半透明にして暗くなったように見せます。要するにクリックされた事が分かればよいのです。
と、その前に今回から数字の画像を平面から立体感あるものにしました。フラットだと見にくい感じなのと、オリジナルのTEN -10-の数字は立体になっているためです。数字を立体的に見せる(ベベル、ふちの立体感を出す)にはPhotoshopで数字が描画されているレイヤーを選択します。選択したらレイヤーメニューから「レイヤースタイル」>「ベベルとエンボス」を選択します。あとは、どのくらい盛り上げるかなどを設定してOKボタンを押せばできあがりです。多少テクスチャを付けるとよいかもしれませんが、今回はテクスチャはなしにしてあります。
また、前回はプログラム内で数字の画像データが読み込まれたらCanvasに描画するようにしていましたが、今回はそれをやめます。数字を描画するたびに読み込み/完了後に処理していると時間かかるしプログラムも面倒だからです。では、どうするかですが、至ってシンプルな方法を採用します。それは数字の画像をあらかじめHTMLファイル内に書いておく、ということです。要するにimg要素で1〜9までの数字を以下のように列記するだけです。

<img src="images/1.png" id="N1"><img src="images/2.png" id="N2"><img src="images/3.png" id="N3">
<img src="images/4.png" id="N4"><img src="images/5.png" id="N5"><img src="images/6.png" id="N6">
<img src="images/7.png" id="N7"><img src="images/8.png" id="N8"><img src="images/9.png" id="N9">


この時にプログラムから扱いやすくするためにID名を付けておきます。このID名は「Nと数字」の組み合わせにしておきます。つまり数字の1の画像ならN1、6ならN6、9ならN9というID名になるわけです。まあ、Nでなくても何らかの英文字であればOKです。ここでのNは単なる識別子ということです。
なお、この方法を採用した場合、完全に画像が読み込まれた後にプログラムを実行する必要があります。これまではいきなり初期化を行う関数initを呼び出していましたが、以下のようにwindowオブジェクトのloadイベントが発生した後に実行するように変更します。onloadプロパティにinit関数を実行するように設定します。一人で作っていて短いプログラムなのでaddEventListener()は使っていません。

init();

window.onload = init; // ●データが完全に読み込まれたら初期化処理を呼び出す

これでプログラム内で数字の画像を描画する処理が簡単になります。そして、もう一工夫します。前回はCanvasで使えるAPI(コンテキスト)を描画するたびに取得していましたが、これをやめてプログラムの最初に1回だけ実行し取得しておきます。以下のようにすることで、いつでも変数(オブジェクト)のctxからCanvasの機能を扱えるようになります。

var canvas = document.getElementById("stage"); // ●canvas要素を取得する
var ctx = canvas.getContext("2d"); // ●2dコンテキストを取得する

すると、どのくらい短くなるかというと数字を消す関数eraseNumberは以下のように実質1行になります。

function eraseNumber(x, y){
ctx.clearRect(x, y, 48, 48); // 指定された範囲を消す
}

数字を描画する部分は以下の2行にまで短くなります。(1行にすることもできます)

var imgObj = document.getElementById("N"+n); // ●HTMLファイル内の画像を指定する
ctx.drawImage(imgObj, x, y); // ●指定した座標に数字の画像を描画する

HTMLファイル内に記述されているimg要素もdrawImage()メソッドの最初のパラメーターに指定できるところがポイントです。というか、Imageオブジェクトならよいので、プログラムで生成しようとHTMLファイルに記述されているものであろうと構わないわけです。このHTML内のimg要素を指定する場合にimg要素にID名を付けておけばdocument.getElementById()のパラメーターに"N"+nを指定することでアクセスできるわけです。

次に数字がクリックされたら暗くする=つまり半透明にする処理を追加します。これは描画する時に不透明度が指定されていればCanvasのglobalAlphaプロパティに、その値を設定するだけです。この時に、元々の不透明度を変数に保存し描画したら戻すようにしておきます。ちなみに、不透明度だけでなくいろいろなプロパティを操作しなければならない場合はsave()、restore()メソッドを使った方が便利です。あと、不透明度は0.0が完全な透明、1.0が完全な不透明ということになっています。
数字を描画する関数drawNumberの3番目のパラメーターに不透明度が指定されている場合は、その値を設定しますが、3番目のパラメーターが指定されていない場合は、どうしたらよいでしょうか。こういうパラメーターの変更を行うとプログラムから呼び出している部分を修正しなければいけなくなります。数カ所ならともかく100箇所もあったら面倒です。検索置換処理でできるならよいのですが、そうでない場合もあります。今回はシンプルに3番目のパラメーターが指定されていない場合は以下のように不透明度100%として処理することにしました。

if (!opacity){ opacity = 1.0; } // ●不透明度が指定されていない場合は100%の不透明度にする

このようにしておけば従来の呼び出し部分も変更せずに済みます。JavaScriptの場合、こういう手法が使われているライブラリやプログラムがありますが、この方法は追加修正に強くないので、あまりお勧めはしません。
まとめると数字を描画する関数drawNumberは以下のようになります。

function drawNumber(n, x, y, opacity){
if (!opacity){ opacity = 1.0; } // ●不透明度が指定されていない場合は100%の不透明度にする
var imgObj = document.getElementById("N"+n); // ●HTMLファイル内の画像を指定する
var saveOpacity = ctx.globalAlpha; // ●現在の不透明度を変数に保存する
ctx.globalAlpha = opacity; // ●不透明度を設定する
ctx.drawImage(imgObj, x, y); // ●指定した座標に数字の画像を描画する
ctx.globalAlpha = saveOpacity; // ●元の不透明度に戻す
}

オリジナルのTEN -10-では数字がタッチされ合計が10の場合、数字が回転して消えます。次回はこのような機能を追加する前に、プログラムを見直してみたいと思います。というのも、このまま作っていくと機能追加に耐えられず破綻してしまう可能性が大だからです。まあ、どのくらい破綻するか見てみたいという人もいるかもしれませんが。とりあえず、少しずつ改良して機能追加に耐えられるようにします。

[サンプルを実行する]

[サンプルをダウンロード]

HTMLファイルの内容

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TEN</title>
<style>
img { width:1px; height: 1px; }
#stage { width : 200px; height : 320px; background-image: url(images/bg.png); }
#clearMsg { position : absolute; left: 10px; top: 200px; font-size: 18pt; color: red; visibility: hidden; }
</style>
</head>
<body>
<h1>TEN</h1>
<div>SCORE : <span id="score">0</span> TIME : <span id="timeString">0</span></div>
<canvas id="stage" width="200" height="320"></canvas>
<div id="clearMsg">ステージクリア!</div>
<img src="images/1.png" id="N1"><img src="images/2.png" id="N2"><img src="images/3.png" id="N3">
<img src="images/4.png" id="N4"><img src="images/5.png" id="N5"><img src="images/6.png" id="N6">
<img src="images/7.png" id="N7"><img src="images/8.png" id="N8"><img src="images/9.png" id="N9">
<script src="main.js"></script>
</body>
</html>

JavaScriptファイル (main.js) の内容

// TEN HTML5 version
// (c) 2014 Cybermuse
// Arranger 古籏一浩
var score = 0;			// スコア(得点)を入れる変数
var count = 0;		// ボタンの合計を入れる変数
var prevNumber = 0;	// 直前にクリックした数字を入れる変数
var prevNumberX;	// 直前にクリックした数字のX座標を入れる変数
var prevNumberY;	// 直前にクリックした数字のY座標を入れる変数
var prevPtr;	// 直前にクリックした数字の配列内での位置を入れる変数
var time = 0;	// ゲームオーバーまでの時間
var timerID = null;	// カウントダウンタイマーのIDを入れる変数
var firstClickFlag = false;	// 最初にクリックしたかどうかのフラグ
var gameFlag = true;	// ゲーム中かどうかのフラグ。trueならゲーム中。
var num = [ ];	// 数字を入れる配列変数を用意
var numXCount = 4;	// 横の数字の個数
var numYCount = 6;	// 縦の数字の個数
var canvas = document.getElementById("stage");	// ●canvas要素を取得する
var ctx = canvas.getContext("2d");	// ●2dコンテキストを取得する
// 初期化処理
function init(){
	canvas.onclick = addNumber;	// canvas要素にクリックイベントを割り当てる
	count = setRandomValue(numXCount, numYCount);	// 戻り値は要素の総数
	time = 30;	// 30秒に設定する
	document.getElementById("timeString").innerHTML = time + "秒";	// 時間を表示する
}
// ランダムに値を設定する関数
function setRandomValue(w, h){
	num = [ ];	// 配列内容を消去する
	var len = w * h;	// 数字の総数
	// ランダムな値を設定する
	for(var i=0; i<len; i+=2){	// 数字の数だけ繰り返す
		var n = Math.floor(Math.random() * 9) + 1;	// 1〜9までの数値をランダムに生成する
		num.push(n);
		num.push(10 - n);
	}
	// 配列要素をシャッフルする
	for(var i=0; i<len; i++){
		var p1 = Math.floor(Math.random() * len);	// 要素の数の範囲で乱数を発生させる
		var p2 = Math.floor(Math.random() * len);	// 要素の数の範囲で乱数を発生させる
		var n = num[p1];	// 2つの要素の内容を入れ替える
		num[p1] = num[p2];
		num[p2] = n;
	}
	// 表示処理
	document.getElementById("clearMsg").style.visibility = "hidden";	// ステージクリアメッセージを消す
	for(var y=0; y<h; y++){	// 数字の数だけ繰り返す
		for(var x=0; x<w; x++){
			drawNumber(num[x+y*w], x*48, y*48);
		}
	}
	return len;	// 数字の総数を返す
}
// 数字を指定座標に描画する
function drawNumber(n, x, y, opacity){
	if (!opacity){ opacity = 1.0; }	// ●不透明度が指定されていない場合は100%の不透明度にする
	var imgObj = document.getElementById("N"+n);	// ●HTMLファイル内の画像を指定する
	var saveOpacity = ctx.globalAlpha;	// ●現在の不透明度を変数に保存する
	ctx.globalAlpha = opacity;	// ●不透明度を設定する
	ctx.drawImage(imgObj, x, y);	// ●指定した座標に数字の画像を描画する
	ctx.globalAlpha = saveOpacity;	// ●元の不透明度に戻す
}
// 指定座標の範囲を消す
function eraseNumber(x, y){
	ctx.clearRect(x, y, 48, 48);	// 指定された範囲を消す
}
// 2つの数字の合計が10かどうか調べる
function addNumber(evt){
	// ゲーム中かどうかを調べる
	if (gameFlag === false){ return; }	// ゲーム中でない場合は以後の処理をしない
	// 初めてのクリックかどうか調べる
	if (firstClickFlag === false){
		firstClickFlag = true;	// クリックされたらtrueにする
		timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
	}
	// クリック座標を求める
	var clickX = parseInt(evt.offsetX / 48);
	var clickY = parseInt(evt.offsetY / 48);
	var ptr = clickX + clickY*numXCount;	// 数字の配列内の位置を計算する
	var clickNum = num[ptr];	// 配列から数字を読み出す
	if (!clickNum){ return; } 	// 数字以外の範囲をクリックした場合は以後の処理をしない
	// 1個目のクリックの場合
	if (prevNumber === 0){
		prevNumber = clickNum;	// クリックされた数字を変数に入れる
		prevNumberX = clickX;	// クリックされた数字のX座標を変数に入れる
		prevNumberY = clickY;	// クリックされた数字のY座標を変数に入れる
		prevPtr = ptr;	// クリックされた数字の配列内での位置を変数に入れる
		eraseNumber(clickX*48, clickY*48);	// 現在の数字を消す
		drawNumber(clickNum, clickX*48, clickY*48, 0.5);	// 現在の数字を半透明で表示する
		return;	// 関数から抜ける
	}
	// 2個目のクリックの場合
	var total = prevNumber + parseInt(clickNum);	// 直前の数値とクリックされた数値を加算する
	if (total === 10){	// 合計が10の場合の処理
		eraseNumber(clickX*48, clickY*48);	// 現在の数字を非表示にする
		eraseNumber(prevNumberX*48, prevNumberY*48);	// 前にクリックした数字を非表示にする
		num[prevPtr] = null;	// 直前の数字を消す。忘れると何度もクリックできてしまう
		num[ptr] = null;	// 数字を消す。忘れると何度もクリックできてしまう
		prevNumber = 0;	// 数値を0にする
		score = score + 1;	// スコア(得点)を追加する
		document.getElementById("score").innerHTML = score;	// スコア(得点)を表示する
		count = count - 2;	// 合計から2を引く
		if (count === 0){	// 全部消したかどうか調べる
			document.getElementById("clearMsg").style.visibility = "visible";	// ステージクリアメッセージを表示する
			clearInterval(timerID);	// タイマーを停止する
			setTimeout(function(){	// 2秒後に表示されているメッセージを消すためのタイマー
				count = setRandomValue(numXCount, numYCount);	// 乱数値を設定する
				timerID = setInterval("displayTimer()", 1000);	// 1秒ごと定期的にタイマー表示関数を呼び出す
			}, 2000);
		}
		return;	// 関数から抜ける
	}
	// 合計が10でなかった場合
	eraseNumber(prevNumberX*48, prevNumberY*48);	// ●前にクリックした数字を消す
	drawNumber(prevNumber, prevNumberX*48, prevNumberY*48);	// ●前にクリックした数字を表示する
	prevNumber = 0;	// 数値を0にする
}
// タイマーの表示処理
function displayTimer(){
	time = time - 1;
	document.getElementById("timeString").innerHTML = time + "秒";
	if (time < 1){	// 時間が1より少なくなったらゲームオーバー
		gameFlag = false;	// ゲーム停止をフラグで示す。falseならゲーム中ではない
		clearInterval(timerID);	// タイマーを停止する
		alert("ゲームオーバー");
	}
}
window.onload = init;	// ●データが完全に読み込まれたら初期化処理を呼び出す