読者です 読者をやめる 読者になる 読者になる

ご注文はうさぎですか?第7羽からインスパイアを受けてHTML5で「ごちうさパズル」を作ってみた

HTML5 JavaScript ご注文はうさぎですか?

ご注文はうさぎですか?第7羽からインスパイアを受けてHTML5で「ごちうさパズル」を作ってみた

ごちうさパズル

ご注文はうさぎですか?の第7羽Call Me Sister.のストーリー中で、ココアがパズルを完成させてしまうというエピソードがありました。

このエピソードからインスパイアを受け、「ごちうさパズル」を作ってみました。

あそびかた

選択した分割数(ピースの数)に応じて画像がパズルのピース毎にシャッフルされます。最大で81ピースまで選べるようにしてあります。ごちうさ本編中では4000ピースのパズルだったので、81ピースはだいぶ少ないですが、なかなか没頭できます。

解くのが面倒になった時は"RESET"ボタンを押すとパズルが元に戻ります。

ごちうさパズルの実装

パズルのエピソードを観た時、HTML5で実装できそうだと思い立ったのですが、実装で地味にハマるところが多かったです。

パズルのピースをシャッフルする際の乱数データの取得方法

パズルのピースをシャッフルする際、乱数を使ってデータをシャッフルしています。パズルのピースは重複しないため、値が重複しないように乱数値をあつめる必要があります。かなり昔のI/Oというコンピュータ雑誌に「重複しない乱数列を得る方法」的な投稿コラムがあったのですが、当時はその原理を理解できませんでした。

が、実際には難しい話ではなく、以下の方法で実現できます。

  • あらかじめ配列に値を入れておく(等差数列でOK)
  • 乱数の値を取得し、配列のインデックスとする(乱数の範囲は0〜(配列のサイズ-1))
  • インデックスが指す値を配列の末尾の値と入れ替える
  • 乱数の範囲を1減らす
  • 乱数の範囲が0より大きい間、処理を繰り返す

JavaScriptによるサンプルは以下のようになります。

var value_list = new Array(10000);

// あらかじめ配列に値を入れておく
for (var i = 0; i < value_list.length; i++) {
    value_list[i] = i;
}

var range = value_list.length - 1;
do {
    // 乱数の値を取得し、配列のインデックスとする
    var index = ~~(Math.random() * range);

    // インデックスが指す値を配列の末尾の値と入れ替える
    var tmp = value_list[index];
    value_list[index] = value_list[range];
    value_list[range] = tmp;

    // 乱数の範囲を減らす
    range--;
} while (0 < range);  // 乱数の範囲が0より大きい間、処理を繰り返す

for (var i = 0; i < value_list.length; i++) {
    console.log(value_list[i]);
}

上記のサンプルを試してみます。配列の中身を一行毎出力しており、単に行数をカウントした値とsort,uniqで重複する値を除去した行数の値が同じであることから、重複しない値であることが分かります。動作環境はNetBSD-6.1-i386です。

$ node sample.js | wc -l
   10000
$ node sample.js | sort | uniq | wc -l
   10000

値もシャッフルされています。

$ node sample.js | head
2537
6218
7624
7580
692
4295
2508
6329
1150
4781

Pemtium M 1GHz,データの件数1万件の場合で0.4秒程度の処理時間です。

$ time (node sample.js 2>&1 > /dev/null)

real    0m0.499s
user    0m0.462s
sys     0m0.020s

$ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 9
model name      : Intel(R) Pentium(R) M processor 1000MHz
stepping        : 5
cpu MHz         : 996.80
fdiv_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 2
wp              : yes
flags           : fpu vme de pse tsc msr mce cx8 apic sep mtrr pge mca cmov pat clflush dts acpi mmx fxsr sse sse2 tm

参考までに、取得した乱数値が重複していないか逐一確認する処理方法の場合と比較してみます。30秒程度かかっており、こちらはオススメできない方法となっています。

$ cat sample-bad.js
var value_list = new Array(10000);

for (var i = 0; i < value_list.length; i++) {
    var index;

    // 配列の値が初期値(-1)でない間繰り返す
    do {
        // 乱数の値を取得し、配列のインデックスとする
        index = ~~(Math.random() * value_list.length);
    } while (value_list.indexOf(index) != -1);

    value_list[i] = index;
}

for (var i = 0; i < value_list.length; i++) {
    console.log(value_list[i]);
}
$ time (node sample-bad.js 2>&1 > /dev/null)

real    0m31.960s
user    0m30.481s
sys     0m0.040s

HTML5 Canvasと画像サイズの関係

ごちうさパズルではピースの数を選択できるようにしています。そのため、画像の幅・高さと1ピースあたりの幅・高さが割りきれないことがあり、ピースを並べて行った結果、Canvasや画像のサイズをわずかにはみ出してしまうことがあります。

今回のケースで考えると、画像サイズは600x600で、パズルのピースはNxNとしており、49pcs(7x7)と81pcs(9x9)の場合に幅・高さが割りきれない値になります。当初、単純に四捨五入(Math.round())する方法をとったのですが、Firefoxでは動作するもSafariではおかしなピースが描画されるという、ブラウザの挙動の違いにハマってしまいました。

$ node
> 600/2
300
> 600/3
200
> 600/4
150
> 600/5
120
> 600/6
100
> 600/7
85.71428571428571
> 600/8
75
> 600/9
66.66666666666667

結果、割りきれないケースの場合は、扱う画像の範囲を少し小さく扱うことで対応しました。

> 600/9
66.66666666666667
> 66*9
594        /* 594pxを画像の幅・高さとする */

Canvas.strokeStyle()の設定

これは私がCanvasの仕様を把握できていなかっただけなのですが、Canvas.strokeStyle()などでストロークの色を変更すると、既に描画したストロークの色も一緒に変更されるようです(Firefoxでしか確認してないので、他のブラウザだと違うかもしれません)。描画したデータについても、内部的には状態を保持しているようです。

まとめ

ご注文はうさぎですか?第7羽のエピソードを元に「ごちうさパズル」を作ってみました。HTML Canvasまわりではブラウザ毎の挙動の微妙な違いにハマってしまいました。Canvasまわりは私の理解が追いついていないところもあり、もう少し調べてみようと思います。