ここまでできる!HTML+CSS 第1回 タイピングゲームを作る
HTMLとCSSの力を最大限に引き出してみましょう。HTMLとCSSだけで実装されたタイピングゲームの仕組みには、さまざまな実装のヒントか隠されているのです。
- カテゴリー
- HTML/CSS >
- HTML/CSSの実践
発行
記事中には先行実装の仕様を取り扱ったものがあります。仕様策定が続いていますので、最新仕様については、仕様書もご確認ください。(2017.07.14)
はじめに
このシリーズでは、HTML+CSSの技術を使った、さまざまな実装アイデアを解説します。JavaScriptを使わずとも、いろいろな表現が可能です。HTMLとCSSを力をどれだけ引き出せるかに重点をおきます。
第1回目はJavaScriptを使わずに、HTMLとCSSのみで実装したタイピングゲームの解説をします。
今回紹介するサンプルは以下のリポジトリからダウンロードまたはクローンできます。
サンプル
まずはサンプルを見てみましょう。NEW GAMEボタンをクリックすると、ゲームが始まります。画面上に表示されたワードを制限時間内にタイピングしていきます。問題をクリアできると、次の問題が表示されます。時間内にすべての問題のタイピングできないと、ゲームオーバーになります。
このゲームはJavaScriptを一切使わず、HTMLとCSSだけで実装されています。
大まかな仕組み
このタイピングゲームはHTMLのForm validationを行って、input要素などに入力された内容を判定し、CSSの擬似クラスで場合に応じた見た目を与えるという実装をしています。
タイピングゲームの仕組みはxl1さんがCSS Programming Advent Calendar 2012の記事で考案したものです*。
*注:オリジナルとの違い
xl1さんの作成したタイピングゲームでは問題がランダムで出題されますが、今回筆者の作成したサンプルでは使用していません。また、ゲームらしくなるように少し改良を加えています。
仕組みの詳細
タイピングゲームの仕組みで重要となる部分を、分解して解説していきます。
:checked擬似クラスと間接セレクタを使用した場面切り替え
以下のサンプルでは、「次の画面へ」をクリックすると、次のシーンへと遷移します。
これを実装するためには:checked擬似クラスと間接セレクタを使用します。
:checked疑似クラスとは、ラジオボタンやチェックボックスがチェックされたときに適用される擬似クラスです。間接セレクタとは、同じ親要素を持つ兄弟要素の弟要素に適用されるセレクタで、一般兄弟セレクタとも呼ばれます。ともにCSS3で定義されています。
*注: :checked疑似クラスと間接セレクタの仕様
さらに詳しく仕様を知りたい方は、Selectors Level 3のThe :checked pseudo-classや、General sibling combinatorを参照してください。日本語の情報としてはMOZILLA DEVELOPER NETWORKの:checkedや、一般兄弟セレクタがおすすめです。
実際のソースコードを見てみます。このサンプルで使用しているHTMLです。
<input id="flag" type="radio">
<div class="scene_1">
シーン1<br>
<label for="flag">次の画面へ</label>
</div>
<div class="scene_2">
シーン2
</div>
label要素をクリックすると、for属性で指定されたラジオボタンが選択された状態になります。ラジオボタンの状態は、:checked擬似クラスを使うことで判定することができます。label要素は表示されていますが、このラジオボタンはクリックされたかどうかの判定にのみ使用するため、画面には表示しません。
/* radioは非表示に */
#flag { display: none; }
/* 見た目 */
[for="flag"] {
color: blue;
text-decoration: underline;
cursor: pointer;
}
CSSでスタイルを適用しリンクのように見せているのが、実はinput要素のラジオボタンであるというのが、このサンプルのポイントのひとつです。
次に遷移前の画面と、遷移後の画面の定義や、ラジオボタンがチェックされたかどうかによってCSSの出し分けをしてる部分を見てみましょう。
[class*="scene_"] {
width: 100%;
height: 0px;
background: #ccc;
overflow: hidden;
}
.scene_1 { background: orange; }
.scene_2 { background: tomato; }
/* チェックされたら色を変更する */
#flag:checked ~ [for="flag"] {
color: #ccc;
}
/* チェックされたら.scene_2を表示 */
/* チェックされていなければ.scene_1を表示 */
#flag:checked ~ .scene_2,
#flag:not(:checked) ~ .scene_1 {
height: 150px;
}
ラジオボタンの:checked擬似クラスの状態(#flag:checked
、#flag:not(:checked)
)と合わせて、間接セレクタによるスタイルの指定をしています。間接セレクタはA ~ B
とすることで、AとBが兄弟関係にあるとき、Aセレクタを持つ要素より後ろにあるBセレクタを持つ要素を選択することができます。
判定に使っているinput要素と、.scene_1
と、.scene_2
のクラス名を持つdiv要素は兄弟関係にあるので、間接セレクタで後続の2つのdiv要素を出し分けています。
このように先頭にinput要素を置いておくと、後続するほとんどの要素に対しての制御を行うことができます。
タイピングゲームでは、ゲームスタート画面からゲームプレイ画面に遷移する部分で、この仕組みを使用しています。サンプルでは、仕組みがわかりやすいようにリンクと同じような単純な切り替えを行いましたが、ページ間のリンクとは異なりますので、一定時間後に切り替えるなどの演出も可能です。これについては後述します。
input要素の入力が正しい場合は次の問題を表示する
タイプされた文字が正解だったら、次の問題を表示するという動作は、input要素に入力された文字列を判定して、さまざまなスタイルを適用することで実現しています。
これを行うためにはHTML側では、入力された文字をinput要素のpattern属性とrequired属性を利用して判定を行います。CSSではその判定を受けて、:valid擬似クラス、:invalid擬似クラスを使用してスタイルを適用します。
次のサンプルを見てください。このサンプルも文字列の判定と、判定に応じたスタイルを適用しています。入力された値が正しい場合は、✔を表示し、次の問題を表示します。
仕組みを見てみましょう。まずは、HTMLです。
<div class="quiz">
1+1=<input type="text" pattern="(2|2)" autofoucs required>
<span></span>
<div class="quiz">
1+2=<input type="text" pattern="(3|3)" autofoucs required>
<span></span>
<div class="quiz">
全問正解!
</div>
</div>
</div>
HTMLのinput要素には、pattern属性とrequired属性が指定されていることに注目してください。この属性は、ぞれぞれ、次のような働きをして、入力された文字の判定を行っています。
- pattern属性:input要素に入力された値と、属性の値として指定された正規表現文字列を比較し、マッチした場合に:valid擬似クラスが有効になり、マッチしない場合には:invalid擬似クラスが有効になります。
- required属性:input要素が未入力な時:invalid擬似クラスが有効になります。
コードでは以下の部分に指定されています。
1+1=<input type="text" pattern="(2|2)" autofoucs required>
pattern属性の値には「半角の2、あるいは全角の2」という正規表現が指定されています。これが入力された場合にのみ、:valid疑似クラスが有効になります。それ以外の文字が入力された場合は、:invaild疑似クラスが有効になります。
また、同時にrequired属性も指定されていますので、フォーム欄が空白のときにもやはり、:invalid疑似クラスが有効になります。
ページが表示された際にinput要素にカーソルが入っている状態にするため、autofocus属性も指定しています。
CSS側では:valid擬似クラスが有効であるか、:invalid擬似クラスであるかで、スタイルを出し分けるようになっています。このサンプルでは、子セレクタ、隣接セレクタ、間接セレクタを使用して、ほかの要素に対して表示・非表示のアクションを起こしています。
*注:子セレクタ
A > B
とすることで、Aセレクタを持つ要素の直接の子要素のうちのBセレクタを持つ要素を選択できます。仕様はCSS Selector Level3のChild combinatorsや、MDNの子セレクタなどを参照してください。
*注:隣接セレクタ
A + B
とすることで、兄弟関係にある要素のうち、Aセレクタを持つ要素(兄)の直後にあるBセレクタを持つ要素(弟)を選択することができます。要素と要素の間に別の要素が入ると、直後の弟要素でなくなってしまうため、適用されなくなります。仕様は、CSS Selector Level3のAdjacent sibling combinatorや、MDNの隣接セレクタなどを参照してください。
/* 正解ならinline-blockにして表示する */
.quiz > input:valid {
display: inline-block;
}
/* 正解のinput要素の直後の弟要素の擬似要素でメッセージを表示 */
.quiz > input:valid + span:before {
content: '✔';
color: green;
}
/* 正解のinput要素以降の兄弟要素の.quizを表示 */
.quiz > input:valid ~ .quiz {
display: block;
}
/* 不正解のinput要素以降の兄弟要素の.quizを表示 */
.quiz > input:invalid ~ .quiz {
display: none;
}
このようにHTMLとCSSが連携することで、タイプした値が正しい場合は次の問題が出現し、フォーカス*があたります。
*注:フォーカスの動作
autofocus属性が指定されたinput要素がiframe要素内の場合、自動でフォーカスが当たらない場合があります。Chrome 31.0.1650.63では、autofocus属性が指定されたinputが複数あった場合は、一番後続のものにフォーカスが当たります。あとからdisplay:block;などで画面に表示されると自動的にフォーカスが移ります。Firefox 26では、後から表示されたものには自動的にはフォーカスが当たりませんでした。そのため、Firefoxではユーザーがクリックしてフォーカスを当てるか、タブキーで移動する必要があります。
回答数に応じて答えを変化させる
間接セレクタを使用し、:valid擬似クラスと:invalid擬似クラスの組み合わせに応じて後続の要素を変化させます。文字を入力させるinput要素それぞれの状態をみて、スタイルを出し分けています。
簡単なサンプルで説明しましょう。
このサンプルでは入力事項の正否はないので、pattern属性は指定していません。required属性を使って、入力されたかどうかを見ているだけです。したがって基本的な仕組みは、前述した「input要素の入力が正しい場合は次の問題を表示する」と同じです。
異なる点は、4つのinput要素が埋まった(:valid擬似クラスが4つ揃った)ときだけ、送信ボタンを表示するスタイルが適用されるようにしている点です。
...
/* すべて正しい場合 */
input:valid ~
input:valid ~
input:valid ~
input:valid ~
.status:before {
color: green;
content:'すべ埋まりました!';
}
input:valid ~
input:valid ~
input:valid ~
input:valid ~
input[type="submit"] {
display: inline;
}
タイピングゲームでも同じような方法で、問題のクリア数を表示しています。さらにすべて正しく入力された場合は、ゲーム終了画面を表示しています。
時間制限とゲームクリア画面を用意する
タイピングゲームでは、すべての問題をクリアするか、制限時間に達したらゲーム終了画面を表示します。時間の管理にはCSS Animationsを使用します。
次のサンプルでは、前述の「:checked擬似クラスと間接セレクタを使用した場面切り替え」で作成したサンプルを、一定時間後に自動で切り替わるように変更しました。
<input id="flag" type="radio">
<div class="scene_1">
シーン1<br>
<label for="flag">次の画面へ ※5秒後に切り替わります</label>
</div>
<div class="scene_2">
シーン2
</div>
HTMLの構造に変更はありません。CSSには時間を管理する部分を追記しました。その部分を見てみます。
...
/* クリックされたら0秒後に非表示するように設定する */
#flag:checked ~ .scene_1 {
-webkit-animation-delay: 0;
}
/* クリックされてなければ、5秒後に非表示するように設定する */
#flag:not(:checked) ~ .scene_1 {
-webkit-animation: 0.1ms hide linear;
-webkit-animation-delay: 5s;
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes hide {
100% {
height: 0px;
}
}
/* クリックされたら0秒後に表示するように設定する */
#flag:checked ~ .scene_2 {
-webkit-animation-delay: 0;
}
/* クリックされてなければ、5秒後に表示するように設定する */
#flag:not(:checked) ~ .scene_2 {
-webkit-animation: 0.1ms show linear;
-webkit-animation-delay: 5s;
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes show {
100% {
height: 150px;
}
}
animation-delay
を使い、5秒後にシーン1の高さを150pxから0pxに、シーン2の高さを0pxから150pxに0.1msかけてアニメーションさせます。CSS Animationsではdisplayの値を変更することができないため、heightを0pxにして非表示にしています。
本来であれば、0pxから150pxにアニメーションしていくのですが、0.1msかけてアニメーションすると、一瞬でシーンが切り替わったように見せることができます。もし5秒経たずに「次の画面へ」が押されたときには、シーンに設定されたanimation-delayを0秒で上書きし、アニメーションをすぐに実行します。
タイピングゲームでも、このanimation-delay
を使い、指定秒後にゲーム終了画面が画面を覆うようにしています。また、10秒未満ですべての問題をクリアしたとき(すべてのinputで:valid擬似クラスが有効な状態)は、animation-delayを0秒で上書きし、アニメーションをすぐに動かします。
そして、ゲームを最初から遊びたい場合は、そのHTMLを再度読み込めばいいので、特に難しいことはせずに、そのHTMLへのリンクを指定します。
擬似セレクタ、属性の実装状況
タイピングゲームで利用している属性や擬似クラス、セレクタの実装状況は次のようになっています。現時点でも、多くのブラウザで使用することは可能ですが、IEをターゲットに入れる場合はIE9以降となります。
名称 / ブラウザ | IE | Firefox | Chrome | Safari | Safari Mobile | Android |
---|---|---|---|---|---|---|
:not擬似クラス | >=9 | ◯ | ◯ | ◯ | ◯ | >=2.1 |
:checked擬似クラス | >=9 | ◯ | ◯ | ◯ | ◯ | >=2.1 |
:valid擬似クラス | >=10 | ◯ | ◯ | ◯ | ◯ | >=2.2 |
:invalid擬似クラス | >=10 | ◯ | ◯ | ◯ | ◯ | >=2.2 |
autofocus属性 | >=10 | ◯ | ◯ | ◯ | - | - |
required属性 | >=10 | ◯ | ◯ | >=6 | - | - |
pattern属性 | >=10 | ◯ | ◯ | >=6 | - | - |
間接セレクタ | >=7 | ◯ | ◯ | ◯ | ◯ | ◯ |
隣接セレクタ | >=7 | ◯ | ◯ | ◯ | ◯ | ◯ |
子セレクタ | >=7 | ◯ | ◯ | ◯ | ◯ | ◯ |
CSS Animations | >=10 | >=25 | >=31* | >=7* | >=3.2* | >=2.1 |
*注:接頭辞
表中に*が付いているものは、接頭辞を必要とします。
まとめ
今回はForm validationやCSSのセレクタを使用し、タイピングゲームを作成しました。このタイピングゲーム自体は実務に使えるものではありませんが、将来的にこのような実装もアリかもしれないと思ってもらえるようなものを作ってみました。
個人的には、HTMLとCSSだけでどこまで行けるのか試してみよう! という遊び心は、自分のスキルを上げるために邪魔になるものではないと思っています。
次回も「このテがあったか」と、楽しんでもらえるような実装をお届けします。
なお、実務で使用するときは、対応しているブラウザや、そもそもの仕様が変わっていないかなどをそのつど調べる必要はあります。ご注意を。