スタイルシート xeyes もどき の説明
戻る


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


カーソル位置の取得

スタイルシートの機能では,直接マウス カーソルの位置を取得することはできません.どの HTML 要素の上にカーソルがあるかは :hover 擬似クラスで判定できますが,カーソルの座標は判りません.
理屈では,カーソル位置を取得したい領域に幅/高さ 1 ピクセルの要素を敷き詰めれば位置が判ることになりますが,それでは位置を取得したい領域が広いと,要素の数が非常に多くなってしまいます.
そこで,段階的に位置を絞り込んでいくことを考えます.

まず,カーソル位置を取得したい領域に,ある程度の大きさの要素をタイル状に並べます.それと,その要素 1 個分の大きさの領域にもっと小さい要素を並べたものを用意します.
タイルの図 1
大きい方の要素のどれかにカーソルが載ると :hover 擬似クラスの条件が発生します.そうしたら,その要素の上に小さい方の要素を並べたものを重ねます.
タイルの図 2
そうすると,今度は小さい方の要素のどれかの :hover 擬似クラスの条件が発生します.
両方の要素の情報を合わせると,カーソルがある位置を小さい方の要素の大きさの分解能で判定できます.

このプログラム(?)では,そのようなことを 3 段階行うことで,カーソル位置をピクセル単位で判定します.
カーソル位置を取得する領域を幅 600 ピクセル,高さ 500 ピクセルの範囲とし,幅/高さ 100 ピクセルのタイルを横方向に 6 個,縦方向に 5 個の計 30 個並べています.
その上に重ねる要素(1 段階小さい要素)は,幅/高さ 10 ピクセルのタイルを横方向/縦方向それぞれに 10 個ずつ,計 100 個並べたものです.
さらにその上に重ねる要素(一番小さい要素)は,幅/高さ 1 ピクセルのタイルを横方向/縦方向それぞれに 10 個ずつ,計 100 個並べたものです.
タイルの図 3


情報の保存

上記のようにしてカーソル位置を絞り込みますが,それだけではまだカーソル位置を取得できません.
小さい方のタイルが上に重なると,大きい方のタイルは :hover 状態ではなくなり,その時点で :hover 条件のセレクタは無効になります.
たとえば 1 番目のタイルの :hover 条件のセレクタに
#elem-a-1:hover ‾ #elem-b {
  --n:1;
}
などと書いて,タイルの番号をカスタム プロパティに設定したとしても,:hover 状態でなくなるとその設定も無効になってしまいます.
上に重ねるタイルの位置を保持したり,カーソル位置に合わせて目玉を描くためには,各段階で調べたカーソル位置の情報を保存しておかなければなりません.
その目的のために transition の機能を流用します.
transition では,プロパティの設定値が変更されてから実際に値が変わり始めるまでのディレイを設定できます.情報を保存したいカスタム プロパティに,このようにディレイを設定します.
transition:--a 0s 10000s;
そうすると,セレクタが条件に該当しなくなっても,ディレイで指定した時間が経過するまでは設定した値が保持されます.ディレイに充分大きな値を指定すれば,事実上設定した値が保存されることになります.
ただし,値を設定するときにディレイが設定されていると,その時間が経過するまで値が変わらなくなってしまいますので,値を設定するときには,transition を無効にするかディレイをゼロにします.

カスタム プロパティに transition を適用するためには,そのプロパティがアニメーション可能である必要があります.
カスタム プロパティをアニメーション可能に設定するのは @ルールの @property で行います.
たとえば
@property --a {
  syntax:'<integer>';
  inherits:true;
  initial-value:0;
}
などのように書きます.syntax<integer><number> などのアニメーション可能なものを指定すれば,そのプロパティがアニメーション可能になります.


情報の設定/継承

調べたカーソル位置の情報を他の要素のプロパティの設定に使うために,以下のような方法で情報を伝えます.

カーソル位置の取得に関係する部分の HTML は以下のような構成になっています.
<DIV ID="co3-00" CLASS="co3" STYLE="left:0px; top:0px"></DIV>    3 段目のタイル(幅/高さ 100 ピクセル)
<DIV ID="co3-10" CLASS="co3" STYLE="left:100px; top:0px"></DIV>
<DIV ID="co3-20" CLASS="co3" STYLE="left:200px; top:0px"></DIV>
    ・
    ・
    ・
<DIV ID="co2">  グループ 2
  <DIV ID="co2-00" CLASS="co2" STYLE="left:0px; top:0px"></DIV>   2 段目のタイル(幅/高さ 10 ピクセル)
  <DIV ID="co2-10" CLASS="co2" STYLE="left:10px; top:0px"></DIV>
  <DIV ID="co2-20" CLASS="co2" STYLE="left:20px; top:0px"></DIV>
      ・
      ・
      ・
  <DIV ID="co1">  グループ 1
    <DIV ID="co1-00" CLASS="co1" STYLE="left:0px; top:0px"></DIV>  1 段目のタイル(幅/高さ 1 ピクセル)
    <DIV ID="co1-10" CLASS="co1" STYLE="left:1px; top:0px"></DIV>
    <DIV ID="co1-20" CLASS="co1" STYLE="left:2px; top:0px"></DIV>
        ・
        ・
        ・

    <DIV ID="iris-r" CLASS="iris"></DIV>  右目の黒目(z-index:-1)
    <DIV ID="iris-l" CLASS="iris"></DIV>  左目の黒目(z-index:-1)
  </DIV>
</DIV>
3 段目のタイルで :hover が発生したら,‾ 結合子を使ってその座標をグループ 2 のプロパティに設定します.
たとえば,3 段目の座標 (100, 200) のタイルで :hover が発生した場合は,このようにします.
#co3-12:hover ‾ #co2 {
  --x3:100;
  --y3:200;
}
ただ,実際のプログラムでは X 座標と Y 座標を別々に設定するようにして,同じ座標の設定はひとつのセレクタにまとめています.
:is(#co3-10, #co3-11, #co3-12, #co3-13, #co3-14):hover ‾ #co2 {
  --x3:100;
}
:is(#co3-02, #co3-12, #co3-22, #co3-32, #co3-42, #co3-52):hover ‾ #co2 {
  --y3:200;
}
グループ 2 では lefttop プロパティを --x3--y3 で設定して,:hover が発生したタイルに位置を合わせます.
left:calc(var(--x3) * 1px);
top:calc(var(--y3) * 1px);
同様にして,2 段目のタイルで :hover が発生したら,その座標をグループ 1 のプロパティに設定し,グループ 1 では :hover が発生したタイルに位置を合わせます.
1 段目のタイルで :hover が発生したら,その座標を黒目のプロパティに設定します.

黒目には次のような経路で情報が伝わります.
3 段目のタイルの座標
グループ 2 に設定される → グループ 1 に継承される → 黒目に継承される
2 段目のタイルの座標
グループ 1 に設定される → 黒目に継承される
1 段目のタイルの座標
黒目に設定される
なお,黒目要素で情報を参照できるようにするため,上記のように黒目要素は HTML 上でタイルの兄弟/子孫要素になっています.標準の Z オーダーでは黒目がタイルより上になるので,カーソルが黒目上にあるとタイルの :hover が発生しなくなってしまいます.それを避けるため,黒目要素には z-index を指定して,タイルより下になるようにしています.


条件分岐

スタイルシートでは,プログラミング言語のように 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 80 以上用に作っていますが,Google Chrome 107 で動くことが確認できています(Opera に比べると動きが鈍いですが).Google Chrome の他のバージョンでの動作は未確認です.
Firefox では,バージョン 105 の時点でまだ @property が未実装なので,動きません.


戻る