スタイルシート ドライブ ゲーム の説明
戻る


JavaScript を使わず,スタイルシートだけでドライブ ゲームを作ってみます.コースから外れないように車を誘導してゴールを目指すゲームです.
全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています.
画面
(これはキャプチャ画像です.これで遊ぶことはできません.)
使い方と仕組みについて説明します.


使い方

画面をクリックするとゲームを開始します.
車が上に向かって進みます.(道が下にスクロールします.)
マウスを左右に動かすと車が左右に動きます.コースから外れないように車を動かして,ゴールまで行きます.
ゴールに到着するかコースから外れるとゲーム終了です.
「車の向きが変わらない」というツッコミはしないでください.


カーソル位置の取得

スタイルシートの機能では,直接マウス カーソルの位置を取得することはできません.どの HTML 要素の上にカーソルがあるかは :hover 擬似クラスで判定できますが,カーソルの座標は判りません.
このプログラム(?)では,幅 2px の要素を横に並べて敷き詰め,:hover 状態になった要素を調べることで,カーソルの X 座標を 2px 単位で取得します.
位置取得用要素
単にカーソル位置を取得するだけなら何の要素を使ってもよいのですが,同時にクリックも判定したいのでラベル(LABEL タグ)を使っています.
ゲームを開始するために画面をクリックしたら,それをラジオ ボタンのチェックで判定します.そのために,ラジオ ボタンにラベルを関連付けて,そのラベルを並べています.(すべてのラベルを同じラジオ ボタンに関連付けています.)


コースの表示

コースを表示するために,高さ 2px の要素を縦に並べます.
コース定義用要素 1
要素のカスタム プロパティでこのように要素の縦方向の位置と道の位置を設定します.
<DIV CLASS="course" STYLE="--x: 90; --y:  0"></DIV>
<DIV CLASS="course" STYLE="--x: 90; --y:  1"></DIV>
<DIV CLASS="course" STYLE="--x: 90; --y:  2"></DIV>
    ・
    ・
    ・
--x: 道の位置, --y: 縦方向の位置
コース定義用要素 2
道の部分を ::before 疑似要素で作ります.そして,元の要素と ::before 疑似要素の背景色でコースを表示します.

コースの長さに応じた個数の要素を並べるのですが,HTML が長くならないよう,この要素を二度使って 2 倍の長さのコースを表示しています.
その際,二度目は道の位置を反転させて,同じ道の形が繰り返さないようにしています.
コース表示 1 左右反転
コース表示 2
つなげて表示する
コース表示 3


コース アウトの処理

車がコースを外れたら,コースのスクロールと車の移動を止めます.このスクロールを止める処理に少々工夫が必要です.
スクロールを行うのにアニメーションを使っていますが,普通のプログラムのように,条件判定の結果でアニメーションを止めるというようなことができません.コースの座標と車の座標から計算で,車がコースを外れたかどうかを示す結果を求めることはできます.しかし,計算した結果は数値ですから,その結果で何かのプロパティを変更しようとしても,数値で設定できるプロパティしか変更することができません.

たとえば,ある条件のとき何かの要素を非表示にしたいとします.セレクタで書ける条件なら visibility:hiddendisplay:none などとすればよいのですが,そうではなく,何らかの計算で判定できる条件の場合,hiddennone は数値ではないので,計算結果で設定することはできません.
代わりに,z-index を変えて他の要素の下に隠したり,widthheight をゼロにしたり,クリップを設定した要素の子要素なら lefttop を変えて親要素の範囲外に追い出したりすることで,表示されないようにすることができます.
しかし,アニメーションの実行状態 runningpaused を変更する操作には代わりになる方法がありません.実行状態を変えるためには何らかのセレクタの状態が変化する必要があります.
そのため,以下のような方法を使います.

初期状態ではアニメーションを停止状態にしておき,上述のカーソル位置取得のための要素(以下「カーソル位置要素」という)が :hover 状態になったらアニメーションを実行状態にします.コースを外れたら z-index で要素の重なり順を操作して,カーソル位置要素が他の要素(以下「マスク要素」という)の下になるようにします.そうするとマウスのイベントがカーソル位置要素に届かなくなり,カーソル位置要素が :hover 状態ではなくなるので,アニメーションが停止します.

ただし,カーソル位置要素の z-index を変えることはできません.コース アウト判定は上述のコース表示のための要素(以下「コース要素」という)で行いますが,カーソル位置要素とコース要素の HTML 上での位置関係は,コース要素がカーソル位置要素より後になります.カーソル位置要素で取得したカーソル位置情報でコース アウトの判定を行うために,そのような順序になります.後にあるコース要素から先にあるカーソル位置要素のプロパティを設定することはできません.そのため,カーソル位置要素の z-index を変えるのではなくマスク要素の方の z-index を変えて,マスク要素が前面になるようにします.

カーソル位置要素とコース要素は HTML 上でこのような配置になっています(実際には間に別の要素もあります).
<LABEL … ></LABEL>  カーソル位置要素
<LABEL … ></LABEL>  カーソル位置要素
<LABEL … ></LABEL>  カーソル位置要素
    ・
    ・
    ・
<LABEL … ></LABEL>  カーソル位置要素
<DIV … >  カーソル位置やコースのスクロール位置を保持する要素 【*】
<DIV … ></DIV>  コース要素
<DIV … ></DIV>  コース要素
<DIV … ></DIV>  コース要素
    ・
    ・
    ・
<DIV … ></DIV>  コース要素
</DIV>
カーソル位置要素の :hover 条件でカーソル位置を上記の 【*】 の要素のプロパティに設定し,コース要素でそのプロパティを継承してコース アウトの判定を行っています.
コース アウトの判定にはコース要素が持っているコースの位置情報のプロパティを使うので,マスク要素の制御はコース要素かその子孫要素で行わなければなりません.その制約から,マスク要素の位置はこのようになります.
<LABEL … ></LABEL>  カーソル位置要素
<LABEL … ></LABEL>  カーソル位置要素
<LABEL … ></LABEL>  カーソル位置要素
    ・
    ・
    ・
<LABEL … ></LABEL>  カーソル位置要素
<DIV … >  カーソル位置やコースのスクロール位置を保持する要素
<DIV … >マスク要素</DIV>  コース要素
<DIV … >マスク要素</DIV>  コース要素
<DIV … >マスク要素</DIV>  コース要素
    ・
    ・
    ・
<DIV … >マスク要素</DIV>  コース要素
</DIV>
マスク要素はコース要素のそれぞれに対して持つことになります.そこで,HTML が長くならないよう,マスク要素は HTML に直接タグを書かずに ::after 疑似要素で作っています.そのため,コース要素は HTML 上ではこのように書いてありますが
<DIV … ></DIV>
内部的にはこのような構造になっています.(実際には,上述のように ::before 疑似要素も使っています.)
<DIV … >::after 疑似要素(マスク用)</DIV>
車がコースを外れてカーソル位置要素が :hover 状態でなくなると,カーソル位置が取得できなくなるので,車の横方向の移動も停止します.それは好都合なのですが,それだけだと車の位置が初期状態に戻ってしまいます.それを防ぐために,:hover 状態でなくなったときのカーソル位置を後述の「情報の保存」の方法で保存して,車の位置を固定しています.


情報の保存

コース アウトの処理で,セレクタで取得したカーソル位置の情報をセレクタの条件が無効になった後も保存しておく必要があります.そのために transition の機能を流用します.
transition では,プロパティの設定値が変更されてから実際に値が変わり始めるまでのディレイを設定できます.情報を保存したいカスタム プロパティに,このようにディレイを設定します.
transition:--a 0s 10000s;
そうすると,セレクタが条件に該当しなくなっても,ディレイで指定した時間が経過するまでは設定した値が保持されます.ディレイに充分大きな値を指定すれば,事実上設定した値が保存されることになります.
ただし,値を設定するときにディレイが設定されていると,その時間が経過するまで値が変わらなくなってしまいますので,値を設定するときには,transition を無効にするかディレイをゼロにします.

カスタム プロパティに transition を適用するために,後述のように @ルールの @property でプロパティをアニメーション可能に設定しています.


カスタム プロパティの定義

このプログラムでは,2 つのカスタム プロパティを @ルールの @property で定義しています.
@property の定義は,たとえばこのように書きます.
@property --a {
  syntax:'<integer>';
  inherits:true;
  initial-value:0;
}
上述したように,情報を保存するために transition を使っている他,コースのスクロールを行うためにカスタム プロパティにアニメーションを使っています.カスタム プロパティにアニメーションを使うためには,そのプロパティがアニメーション可能である必要があります.
@propertysyntax<integer> を指定することで,そのプロパティをアニメーション可能にしています.


条件分岐

スタイルシートでは,プログラミング言語のように if 文などで条件を判定して処理を分けるようなことができません.プロパティの設定はすべて式として書かなければなりません.また,直接数値の大小を比較するような機能もありません.
そのため,条件によって値を変えるところは,すべての条件の場合の値を計算しておいてから,そのうちのどれかが選ばれるような式を書くような形になります.C 言語の条件演算子(? :)や Visual Basic の If 演算子のようなものがあれば便利なのですが,今のところそのような機能は無いようです.

たとえば --x に,ある条件を満たす場合は --n1,満たさない場合は --n2 の値を設定するという場合,条件を満たす場合に 1,満たさない場合に 0 となるようなプロパティ --b を計算しておいて
--x:calc(var(--b) * var(--n1) + (1 - var(--b)) * var(--n2));
と書きます.
--b はプログラミング言語で言うブール変数に相当します.
--b の計算の仕方は,たとえば --v が整数を表すプロパティである場合,その値が 42 に等しいかどうかを表す --b はこのように計算できます.(小数の場合はもう少し面倒です.)
--w:calc(var(--v) - 42);
--b:calc(1 - min(var(--w) * var(--w), 1));
こんな風にも書けます.
--w:clamp(-1, var(--v) - 42, 1);
--b:calc(1 - var(--w) * var(--w));
abs() が実装されていれば,もう少し簡単に書けます.
--b:calc(1 - min(abs(var(--v) - 42), 1));
--b1--b2 を条件の真偽を表すプロパティ(1/0)とすると,論理演算は次のように計算できます.
論理積(AND)
  var(--b1) * var(--b2)
論理和(OR)
  min(var(--b1) + var(--b2), 1)
否定(NOT)
  1 - var(--b1)
このような考え方で,条件分岐的な処理を力業で書いています.


このおもちゃは Opera 76 以上用に作っていますが,Google Chrome 107 で動くことが確認できています.Google Chrome の他のバージョンでの動作は未確認です.
Firefox では,バージョン 105 の時点でまだ @property が未実装なので,動きません.


戻る