🚀

JavaScriptゼロ!CSS(とHTML)だけで作るリバーシゲーム

に公開

はじめに

こんにちは!jig.jp Engineers' Blog Advent Calendar 2025 の12/24の記事を担当します たくとです!
今回はHTML, CSSを使ってリバーシを作ってみました。
※ 本記事で紹介するページは、Safari, Firefoxでは現在動作しません。(Chrome, Edge, Operaでは動作することを確認しました。)

まず、今回作成したものと、そのHTMLhead部分を見せます。

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>リバーシ</title>
  <link rel="stylesheet" href="style.css">
  <link rel="stylesheet" href="script.css">
</head>

はい、ということで、異常に気付いた人もいるかもしれません。
script.css」って何?って感じですよね。ということで、今回はJavaScriptを一切使わずに遊べるリバーシを作ってみました٩( 'ω' )و

では、まずは一番短いHTMLから紹介していきます!

HTML

概要

HTMLの構造はこのようになっております。

.reversi>.scan.WH>.scan.LR>.scan.RL>.turn>(.board.lr>div*64)+.board.rl>div*64

.reversiから.scan系の.WH, .LR, .RLが重ねられ、.turnを挟んでその下に.lr型と.rl型の.boardが重なっているような感じになります。
結構単純ですよね!
これでHTMLの解説は終わりです!

軽く紹介

残り2ファイルですがstyle.cssの解説に行く前にまずは、今回使用した中であまり普段見かけなそうだったり詳しくは知らなそうなプロパティやアットルールなどをまとめました。長くなったので飛ばしてください。気になったら見る感じで。

補足(単位・プロパティ等)

単位
  • ch:文字の0の横幅を1とした単位(00の横幅は2chとなる)
  • dvmin:全体のサイズによっていい感じに変えてくれる単位(画面の動的な縦横の小さい方を100dvminとした割合の長さ)
  • frgridflex内の要素で有効な単位(比率になっていて1frと2frと3frのものがあれば残りの空間を1:2:3に分けるような感じにできる)
プロパティ
  • aspect-ratio:要素の縦横比を指定できる(縦横の片方が決まるようにしておけばもう片方はこの比率に合わせて変わる)
  • position: relative;position: absolute;relativeはレイアウトに影響し、absoluteは影響しないですが、relativetopleftを使っても他の要素のレイアウトに影響を与えません(topleftを使っても変わるのは見た目だけ)
  • pointer-events: none;pointer-events: none;:activeが有効な状態で設定しても切れません。:hoverは切れます(なので、:hover:activeとするとpointer-events: none;で切ることのできるセレクタとなる)
  • letter-spacing:文字の間隔を調整できる。現在の間隔に±どれだけするかという調整方法
  • white-space: pre;:文字をそのままにするような設定。地味な機能だが、::before::afterなどのcontent'\A'という文字を使って改行できるようになる効果もある。
  • line-height:1行の高さを指定できる(3行あってline-height: 20px;とすると全体で高さが60pxになる、0pxを指定すると改行しても高さが変わらないようにできる)
  • paddingとgapflexgrid要素内でpaddinggapを同じ値にすると簡単に均一な空間を開けることができる。
  • writing-modegrid要素でsideways-lrsideways-rlなどを使うと通常は配置するのが難しいような順番で要素を並べることができる。(horizontal-tbが通常の順)
  • counter-resetcounterを好きな名前で何個でも好きな初期値で作成できる。これを指定した要素内ならどこでも有効。counter()関数で文字列型で値を出力できる。
  • counter-increment:作ったcounterの値から±好きな数で好きな数だけ何個でも増減させることができる。
  • animation-play-state:アニメーションの進行を停止させたり再開させたりできる。デフォルトをpausedで停止させておくと、押している間だけアニメーションさせて押してない間は保持させたりできる。
  • animation-composition:アニメーションの適用方法を変えることができる。指定無しの場合replaceとなり、上書きでアニメーションする。addを指定すると現在値に加算する形で指定できる。
  • animation-range:アニメーションがview()やscroll()の場合に効果を発揮する。containcoverなどを指定でき、containがスクロール親要素内の範囲だけの領域で、coverは完全に隠れるまでのスクロールの領域のことである。
関数
  • radial-gradient():円形にグラデーションできる。滑らかに変えたり、瞬時に切り替えたり、基準を変えたりできる。一度に複数できるバージョン(repeating-)も存在する。(指定が難しく、慣れるまで使いづらい)
  • calc():数式(四則演算)を計算させる関数。単位を割り算で取り除くことができる。
  • counter():カウンターの値を文字列として取得できる関数。好きなスタイルを指定できる。
  • clamp():数値や長さを範囲に収める関数。3つの値を使ってclamp(2, 7, 4)のように指定した場合、2~4の範囲に7を収めるので4が返る。
  • if()とstyle():CSSのif文は少しクセが強い。CSS変数に対して使用でき、以下のような感じに指定できる(重ねて使用することも可能)。and, or, notが使える。通常計算値は使用できないが、@propertyを使って定義しておくと計算値でも判定が有効になる。
--apple: if(
  style((--eat: 1) and (--has: 1)): red;
  style(--poison: 1): purple;
  else: none;
);
  • mod():余りを求めることができる。(mod(19, 3)1)
  • round():なんと切り下げ(floor())も切り上げ(ceil())も四捨五入も全部これに詰まっている。round(down, 13 / 4)のようにすると切り下げになる。
  • sibling-index():現在の要素の親から見た位置を整数値として返すことができる。セレクタにある:nth-child()の上位互換のようなもの。
  • view()view()アニメーションを設定する。animation-timelineで指定する。view()アニメーションは一番近いスクロール親を探してそれに対してアニメーション進行度を設定する。縦か横の2方向で指定できる(斜めはできない)。overflow:scrollautoまたはhiddenにすることでスクロール親にできる。
アットルール
  • @counter-stylecounter()で指定できるスタイルを設定できる。system:で挙動を選択できるが、symboliccyclicが使いやすい。symbols:で登場させる文字を登録でき、なぜかこれで登録した文字は120が上限となっていた(120を超えると普通の数字で121のように表示される)。range: 1 6;のようにrange:を使うと有効な範囲を指定できるが120は突破できない。fallback:range:に入らなかった場合のスタイルを指定できる。
  • @propertyCSS変数を型や初期値などを指定して作ることができる。syntax: "<integer>";のようにして型を指定でき、inherits: true;で子孫の要素に値を伝搬させるかを指定できる。initial-value: 3;のようにして初期値も指定できる。型を定義することでアニメーションで滑らかに値を変化させれるようになったり、if文の判定で計算値でも利用できるようになったりする。

style.css

style.cssの機能はこのようになっております。

概要

  1. .reversi, .boardのサイズを盤面と同じにする
  2. .reversiは押せなくする
  3. :activeは表示を上にする
  4. セルの::beforeは押せるようにする
  5. 盤面の配置を調整する
  6. 見た目を整えて色をつける
  7. position: absolute;を設定する
  8. 情報を上手く渡せるように加工する
  9. 1つしか表示できなくする

1つずつみていきます

詳細説明

1. .reversi, .boardのサイズを盤面と同じにする

そのまま見た目を調整しています。今回はdvminという単位で調整してみました!
dvminvwvhの仲間で、dはダイナミックです。スマホなどで、スクロールした際などに、urlの部分が勝手に下がってきて画面サイズが変わってしまったりすると思います。そのような際にもその変化した後のサイズで調整できるものとなっています。

2. .reversiは押せなくする

pointer-events:は子に伝搬するので、.reversiを押せなくすることで子要素も全部押せなくできます。

3. :activeは表示を上にする

こうすることで、判定の補正の時に上手くいきます。詳しくはscript.cssの最後の方で解説します。

/* 押されたらその要素で:hoverも発動させれるように上に上げる */
.board > div:active {
  z-index: 999;
}

4. セルの::beforeは押せるようにする

これを置けるマスにだけ表示させるので、ここを押せるようにします。

5. 盤面の配置を調整する

writing-mode:sideways-lrsideways-rlを使用して完全に逆の方向から配置させました。通常のwriting-mode:でもorder:sibling-index()を使って計算した値を使用することでできはするんですが、この方法の方が単純に作ることができます。
sideways-lr

↑↑↑↑
↑↑↑↑
↑↑↑↑
↑↑↑↑
↑→→→

のような感じで配置されます。

sideways-rl

←←←↓
↓↓↓↓
↓↓↓↓
↓↓↓↓
↓↓↓↓

のような感じで配置されます。

6. 見た目を整えて色をつける

padding:gap:, border:border-radius:, aspect-ratio:などでいい感じの見た目に調整しました。色は、各セルのbackground:に対して、radial-gradient(closest-side, var(--color) 75%, darkgreen 75%)を指定し、--colorblack, white, darkgreenを切り替えることで簡単にセルの更新ができるようにしました。

7. position: absolute;を設定する

親と子要素のposition:を全部absoluteにすることで親のサイズは0になりますが、1つだけレイアウトに影響するものにしておくと親も子のサイズと同じにすることができます。::afterにそれを担当させることでただ重ねるだけで何層でも可能になります。

8. 情報を上手く渡せるように加工する

5.でサイズを親に反映させれるようにしたが、今度は、letter-spacing: calc(1px - 1ch);white-space: pre;line-height: 1px;を指定することで、0 m文字m pxに、\A n文字n pxにすることができます。つまり、親のサイズを::afterに従うようにして、::after情報を詰め込むことで親のサイズに情報を反映させることができます。

9. 1つしか表示できなくする

line-height:0に設定します。改行しても行の高さを変えないようにすることでXXXXXを1文字ずつ改行すると全て重なったXとなり、複数あることを認識できなくできます。それだけでは単純にheight:0になり、文字が上の要素に重なるようになってしまうので、padding-block: 0.5em;で、本来の位置に再調整します。さらにmargin:も設定して微調整します。こうすることで、XXXXXを1文字ずつ改行すると普通のXに見せることができます。

さあこれにて、style.cssが終わりました!ところどころ変なやつがいたかもしれませんが、ここからが本題です!残すはあとscript.cssだけ!

script.css

script.cssの機能はこのようになっております。

概要

  1. .reversiをスクロール親に設定して、中のすべての要素のview()の基準にする
  2. sibling-index()で各セルの座標(--w--h)を求める
  3. 初期配置を作る
  4. --w--hから斜め座標--p--nを求める
  5. --w, --h, --p, --nを使ってcounterをセルごとに変える
  6. .lr, .rlのセルのcounterを駆使して4方向ずつ、8方向の条件判定を行い、置けるセルなら::before置けると表示する
  7. 押しているセルから.WH::afterに座標を横流しする
  8. 押している座標を.WHから全体に伝える
  9. .turnでターンを切り替える
  10. 押した際に、--turnによって黒か白を置くアニメーションの再生を切り替えて実行する
  11. .lr, .rlのセルの::afterから.LR::after, .RL::afterへ4方向ずつ横流しする
  12. 8方向それぞれで反転できるマス数を.LR, .RLで全体に伝える
  13. 反転処理を行う
  14. 黒か白が置かれる前に反転の処理を実行する理由(順番が重要:反転→配置→ターン切り替え)
  15. --guideを同時にアニメーションさせ、反転されても--guideの値を使用することで置く条件が解除されないようにして、なるべく安定させる
  16. ターンの切り替えも同様に、--guideを利用して、駒が置かれてもターンが切り替わるまではアニメーションするようにしてなるべく安定させる

1つずつみていきます

1. .reversiをスクロール親に設定して、中のすべての要素のview()の基準にする

animation-timeline:で使用されるview()はスクロール親の可視領域に対する表示割合を基準にアニメーションを行うものです。overflow:hiddenauto, scrollに設定することでスクロール親にすることができます。スクロール親の子孫でview()を使うことができます。
本来はスクロールさせてそれに合わせて変えるような使い方をするんですが、今回は位置を固定させて逆に子のサイズを変えることで割合を変えるという方法を行います。(可視領域で変わるのでサイズを連動させれば値を好きに調整できる)

2. sibling-index()で各セルの座標(--w--h)を求める

実は座標の取得自体はview(x)view(y)の2つを同時に使って行うことも可能です。しかし今回はこの方法を使えません。駒の配置処理ではanimation-play-state:を切り替える必要があるのですが、同一要素に複数のアニメーションを設定している場合、その要素のanimation-play-state:に対して自身のいずれかのアニメーションで更新された値を使おうとすると(たとえcalc()if()などで加工した値であっても)無効になる挙動になりました。(詳細はこのテストコードに)
そこで今回は、view()を使わずアニメーション無しで座標を計算できるsibling-index()を採用しました。(sibling-index()は1スタートなので-1して0スタートにします)

/* .lr側の式 */
--w: round(down, (sibling-index() - 1) / 8);
--h: calc(7 - mod(sibling-index() - 1, 8));
/* .rl側の式 */
--w: calc(7 - round(down, (sibling-index() - 1) / 8));
--h: mod(sibling-index() - 1, 8);

3. 初期配置を作る

--w--hが出せたところで初期配置を作れます。中心の4マスを黒と白にします。左上が原点なので、3と4を使って、以下のようになります。0が緑、1が黒、2が白です。

.board > div {
  --init: if(
    style(
      ((--w: 3) and (--h: 3)) or
      ((--w: 4) and (--h: 4))
    ): 1;
    style(
      ((--w: 3) and (--h: 4)) or
      ((--w: 4) and (--h: 3))
    ): 2;
    else: 0;
  );
}

これを色に変換し、表示させるとこのようになります。

--color: if(
  style(--init: 1): black;
  style(--init: 2): white;
  else: darkgreen;
);
background: radial-gradient(closest-side, var(--color) 75%, darkgreen 75%);

4. --w--hから斜め座標--p--nを求める

--pは\の斜めで、--nは/の斜めとしました。数学のグラフを思い浮かべると、式はy=-x+nの関数とy=x+nの関数なので、x+yx-yで対応できます。判定させる必要のあるのは列に存在する要素が3つ以上の部分だけでいいので、統一感を出すために初期値を+5-2で調整し、どちらも0~10の範囲で対応させれるようにしました。

/* マスの斜め\座標 */
--p: calc(var(--w) - var(--h) + 5);
/* マスの斜め/座標 */
--n: calc(var(--w) + var(--h) - 2);

5. --w, --h, --p, --nを使ってcounterをセルごとに変える

以下のように、--w, --h, --p, --nを用いて、黒用にbと、白用にwで、(w, h, p, n)×(b, w)の組み合わせ8種類(--wb, --ww, --hb, --hw, --pb, --pw, --nb, --nw)を作成しました。(w4bw方向での4列目の用といった感じに考えた)

--wb: if(
  style(--w: 0): w0b;
  style(--w: 1): w1b;
  style(--w: 2): w2b;
  style(--w: 3): w3b;
  style(--w: 4): w4b;
  style(--w: 5): w5b;
  style(--w: 6): w6b;
  else: w7b;
);

こうして作った8つのカウンター名をcounter-reset: var(--wb) var(--ww);のように設定することでセルごとに対応するカウンターの値を対象に操作することができます。

counterを使って判定を行う理由

counterには他にはないすごい性質があります。counter-resetした要素の子孫とそれ以降の要素に全てまでに、情報を変更しながら伝えることができます。この特性はおそらくcounter以外で作るのはものすごく難しいです。また、リバーシの判定は相互のマスが影響し合うので合っているから相性がいいというのもあります。

6. .lr, .rlのセルのcounterを駆使して4方向ずつ、8方向の条件判定を行い、置けるセルなら::before置けると表示する

4.で列ごとの使うカウンターの分離はできたので、ここでは1列で考えます。

1列で取れる数の範囲

まず、counterの値は、取得できたマス数にしておくと作りやすいので、リバーシでは1列で最大何マス取ることができるかを考えます。
1列には8マス存在し、黒白白白白白白緑の状態で黒を置くのが最も多く取れます。
なので、取れる最大数は6です。また、黒緑の状態では取れないので、1が最低となります。
取れる範囲は1~6に決まりました。

黒のターンだとして、黒緑緑黒白白白白白白緑の盤面を使いながら、判定ロジックを考えていく

逆算する形で求めます。

逆算

  • この盤面では、緑のマスで06となるようにすればいいということになる。
  • 黒が判定の開始位置なので、黒でcounter-reset:を行う。
  • 白は判定の途中で、緑は判定の終了になる可能性があるので、counter-reset:せず、counter-increment:を行う。
  • 白をカウントするたびに取れるマスは1ずつ増えるので、白では+1を行うものとする。

初期値をn、緑ではm変わるとして、以下のようになればいいです。

n 0(=n+m) m
n n+1 n+2 n+3 n+4 n+5 n+6 6(=n+6+m)

ここから、nとmはちょうど正負逆の値にすればいいこと、mは1~6よりも大きくする必要があると分かります。
これらの条件を満たすならなんでもいいですが、例えばm=7とすると、n=-7となり成功します。

-7 0 7
-7 -6 -5 -4 -3 -2 -1 6
CSSで記述する場合はこのようになる
/* --stateは1が黒、2が白、0が緑に対応している */
--counter-reset: if(
  style(--state: 1): var(--wb) -7 var(--hb) -7 var(--pb) -7 var(--nb) -7;
  style(--state: 2): var(--ww) -7 var(--hw) -7 var(--pw) -7 var(--nw) -7;
  else: none;
);
counter-reset: var(--counter-reset);

--counter-increment: if(
  style(--state: 1): var(--ww) var(--hw) var(--pw) var(--nw);
  style(--state: 2): var(--wb) var(--hb) var(--pb) var(--nb);
  else: var(--wb) 7 var(--ww) 7 var(--hb) 7 var(--hw) 7 var(--pb) 7 var(--pw) 7 var(--nb) 7 var(--nw) 7;
);
counter-increment: var(--counter-increment);

これで取るマスを計算するロジックは完成です!

範囲内なら置けるを表示する

ここまでで取れるマスの値にcounterの値を加工することができました。次は置けると表示させる方法を説明する。@counter-styleを2つ使って範囲内なら置けるを表示させます。
まずは完成形を見せます。

/* 取れるマス数の範囲 */
@counter-style valid {
  system: cyclic;
  symbols: "置ける\A";
  range: 1 6;
  fallback: invalid;
}
/* 無効 */
@counter-style invalid {
  system: cyclic;
  symbols: "";
}

counter()validというスタイルを使うだけでできるようになっています。
まず、system:というのはcounterの表示方法についての設定で、cyclicは設定されているカウンターの文字列をループして繰り返すというものです。symbols:ではカウンターで使う文字列を指定します。
つまり、今回はsymbols:"置ける\A"だけしか設定していないので、常に置けるを表示するだけのカウンターになります。(\Aはすぐ後で解説する)
range:は有効な範囲で、fallback:は無効な場合の代わりとなるカウンターです。range: 1 6;なので、1~6で"置ける"、他の数字が来た場合には""になります。

counter()を表示させる記述はこのようになります

/* --state0は緑のマスという意味 */
--place: if(
  style(--state: 0): if(
    style(--turn: 0): counter(var(--wb), valid) counter(var(--hb), valid) counter(var(--pb), valid) counter(var(--nb), valid);
    else: counter(var(--ww), valid) counter(var(--hw), valid) counter(var(--pw), valid) counter(var(--nw), valid);
  );
  else: none;
);
content: var(--place);

これを見ると分かる通り、連続して並べています。なので、例えば2方向取れるような場合は表示が"置ける置ける"のようになってしまいます。しかし、ここで\Aがこの問題を解決してくれます。white-space: pre;を設定している場合は"\A"を改行として扱うという特殊モードになります。そのため、style.css 9.で解説した改行高さを0にする裏技をここに適用することで、何列判定が重なっても表示を単一の"置ける"にすることができるようになります。

counter()DOMの後ろ方向にしか伝搬できないので、盤面を.lr.rlの2枚に分け、.lr(w, h, p, n)=(←, ↓, ↖︎, ↙︎).rl(w, h, p, n)=(→, ↑, ↘︎, ↗︎)を担当させて合計8方向を判定するようにしています。

7. 押しているセルから.WH::afterに座標を横流しする

押しているセル

押しているセルは普通、.board > div:activeというセレクタを使うんですが、今回は.board > div:hover:activeを使用します。:hoverをつけることで、pointer-events: none;を付与したときの挙動が変わります。:activeな状態でpointer-events: none;をしても:activeのままになります。どうやら一度判定されるとマウスが離されるまで発動するようで、うまくいきません。これでは意味がないです。しかし、:hoverpointer-events: none;で解除されます。なので、この2つを組み合わせることで、pointer-events: none;で解除できるバージョンの:activeを作ることができます。

横流し

その上で、このセレクタで駒を置くアニメーションをさせればいいかと思いきや、今回は2つの.board(.lr.rl)を連動して動かす必要があります。なのでここでアニメーションを開始するのは微妙です。
どこを押しているかを全体に届ける必要があります。そうすることで、届けられた座標が今のセルと一致している場合のみアニメーションさせるようにすると、同じ階層の深さにあるので同期して動かすことができます。
しかし直接届けることはできない(親は押しているセルの座標を知りたいがどのセルがそうなのか特定できない)ので、まず、その準備として押しているセルが主導となって.WH::afterに届けるようにします。そして親は::afterから取ればいいです。
.WHでそれ用のカウンターWHcounter-reset:します。それを使ってこのようになります。(このカウンターWHの使い道などは次の8.で話します)

.board > div:hover:active {
  counter-increment: var(--counter-increment) WH calc(8 * var(--h) + var(--w) + 1);
}

ここで、var(--counter-increment)は設置判定に使っていたやつで、押しているマスでも判定はしていないとだめなので必要です。それに加えてという形で、calc(8 * var(--h) + var(--w) + 1)ですが、情報数は少ない方があとあと楽なので、1つの値に圧縮しておきます。ここで+ 1には重要な理由があります。マスの座標は0基準で考えていたので、マスには(0, 0)も存在します。もしこの+ 1がなければ、ここのマスを押しても値は0なので反応しません。そこで+1をしておくと、どうなるかというと、まず、押していない場合、何もWHincrementされないのでカウンターの値は0のままで、何もないです。そして値0のマスを押すと、このセレクタが発動するので、WHの値は1になります。このように1ずれた値として保存できるようになるんです。(なので、WHに入るのは0~64までの値になります)

8. 押している座標を.WHから全体に伝える

.WH::afterのサイズに情報を詰める

6..WH::afterに送られたカウンターのWH.WH::afterの横幅に設定します。以下のカウンタースタイルを使用します。

@counter-style zero {
  system: symbolic;
  symbols: '0';
  fallback: invalid;
}

今度はsystem:の値がsymbolicです。これは先ほどのcyclicとは違って、symbols:を繰り返すのは同じですが、1周すると追加する(数字みたいな感じ)。といったものになります(もっと数字に近い挙動のnumericというものもありますが若干違うだけでほぼ同じです)。今回もsymbols:は1つだけなので単純で、'0', '00', '000', ...のように増えていく形になります。(counter値が0ならinvalidで''になる)
これにより、counter値を文字の数と同じにし、横幅に比例させることができます。さらに、使用している文字は'0'だけなので、style.css 8.で解説した、letter-spacing: calc(1px - 1ch);が利用でき、これにより比例ではなく同じにすることができます。

実際の.WH::afterは次のように、WHを不可視の文字列として出力します。

.WH::after {
  content: counter(WH, zero);
  letter-spacing: calc(1px - 1ch);
  color: transparent;
  display: block;
}

親のサイズに移す

さらに、style.css 7.によって、.WHのサイズを.WH::afterと同じにできます。

親のサイズをCSS変数に移す

親のサイズまで変えれても、子はそれをCSS変数として使えないので、ロジックに利用できないです。CSS変数にも移す必要があります。

ここで欲しいのは.reversi(固定500px)に対する.WHの現在幅の比率 です。
この比率をview(x)の進行度として読み取り、数値(0〜500)に変換して--scanに入れます。

ここで、1.で行ったoverflow: hidden;が生きてきます。このサイズは盤面なので、固定です。これを基準に現在のサイズをview(x)を使って取得することができます。
具体的には、view(x)で動かす、データを受け取るためのアニメーションscanを以下のように設定します。(ここでは簡単のため、盤面のサイズを500pxとします)

.WH {
  animation: scan linear reverse;
  animation-timeline: view(x);
  animation-range: contain cover;
}
@keyframes scan {
  to {
    --scan: 500;
  }
}
@property --scan {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

まず、軽く流れを説明し、その後reverse, animation-range: contain cover;を解説します。
横方向にview()を適用し、全体を500としたうちの現在のサイズを割合で数値として--scanで取得します。

reverse

今回はview()を特殊に利用しているからです。普通のサイトの場合、スクロールしたら下から上に要素が出現していきます。なので、view(y)は下を基準に上に対してアニメーション進行度が反映されるようになっており、それはview(x)でも同様です。横スクロールの場合、普通は右にスクロールしていくので、右側に始まりがあり、要素が左に行くにつれてアニメーションが進みます。でも、今回は普通に配置し、要素を伸ばしたりして値を取得するので逆で、左から右の流れになっています。なので、reverseにしておくと直感通りの向きになります。

animation-range: contain cover;

これも普通のスクロールをイメージして、これの逆のcover containから考えたほうが理解しやすいですが、まずはanimation-range:から。
animation-range:view()のアニメーションの開始位置と終了位置についての設定です。これは言葉でうまく説明するのは難しいので、このサイトで色々試しながらがおすすめです。
まず、view()はざっくり自分が親の始まりから親の終わりまでを移動するアニメーションだと思いましょう。
containは簡単に言うと完全に入り切ってから終わりに触れるまでの割合です。
coverは簡単に言うと見え始めた瞬間から完全に出切るまでの割合です。
cover containとすると、見え始めた瞬間から終わりに触れるまでの割合になります。

これを踏まえて元の考え方

そして、reverseにしているので逆になるので、contain coverはこれも逆の逆で元に戻って、見え始めた瞬間から終わりに触れるまでの割合と考えましょう。
すると、範囲を考えると、50pxのものだった場合、中に入らずピッタリ接している状態から、親サイズの500px移動させると中でちょうど収まったまま反対側に接するのでこの500pxというのが範囲となります。
よってこれをはみ出さずピッタリ中に配置するということは、中に入らずピッタリ接している状態から50px移動させた状態にあたるので値として50が得られるのです。
つまり、ややこしいですがこのようにすることでそのままサイズを得られるということです。

データの展開

さて、これにて--scanに横幅(横流しされた--w--hが圧縮された値)を入れることができました。展開すれば全体で使いやすい値--W, --Hにできます。
ここは簡単です。ただ、+ 1していたことを忘れず、ちゃんと- 1して調整しましょう。

--W: mod(var(--scan) - 1, 8);
--H: round(down, (var(--scan) - 1) / 8);

これで、押しているセルから座標の値をcounter.WH::afterに伝えてそれを横幅に適用し、それを親の横幅に反映させて、値として取得し、全体に伝えることに成功しました!!

9. .turnでターンを切り替える

さて、.7で押しているセルの座標を--scanで1つのデータで受け取って、--W, --Hとして使いやすい形でも全体に伝えることができるようになりました。
ここで、--W, --Hにはもちろん使い道しかありませんが、--scanにもまだ使い道があります。--scanは押していない間は0になります(押すと1~64)。なので、押しているかどうかはこちらを使う方が分かりやすいです。

.turnでももちろん使えるので、ターンの切り替えに--scanを使用します。
style.css 4.より、押せるのは::beforeだけです。
そして、ここでポイントは、::beforeに対して行っても、そのセル自体に:hover:activeもつくということです。
なので、--turn0, 1で切り替えるようにして、0なら黒の判定処理、1なら白の判定処理に切り替えるようにすることで、押すことで:hover:activeを発動させ、--scanが0以外になり、アニメーションさせれば勝手にターンが進み、(ターンが変わることで判定処理が相手のものに切り替わるので)そのマスの::beforeは自動で消え(判定も消え)、--scan0になり、相手のターンに変わるようにできます。

なので、--scanによって変えればいいのでこうです。

.turn {
  animation: turn linear 750ms infinite;
  --aps: if(
    style(--scan: 0): paused;
    else: running;
  );
  animation-play-state: var(--aps);
}

10. 押した際に、--turnによって黒か白を置くアニメーションの再生を切り替えて実行する

押しているマスの特定

まずは押しているマスかどうかを判定します。以下の値を使います。

.board > div {
  --wW: calc(var(--W) - var(--w));
  --hH: calc(var(--H) - var(--h));
}
@property --wW {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --hH {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

この値は全体に公開された--W, --Hがそのセルの配置に一致していれば0になります。if()で使いやすい形になります。(@propertyで設定し、if文でも使えるようにします)(.board > div:hover:activeで指定せず.board > divで指定しているのは.lr, .rlのうち押していない側でも判定を有効にするためです)
この--wW, --hHの両方が0のセルが、押しているマスになります。

黒白の切り替え

以下が完成形です。

.board > div {
  animation: gb linear 300ms forwards, gw linear 300ms forwards;
  --aps1: if(
    style((--wW: 0) and (--hH: 0)): if(
      style(--turn: 0): running, paused;
      else: paused, running;
    );
    else: paused, paused;
  );
  animation-play-state: var(--aps1);
}
@keyframes gb {
  to {
    --first: 1;
  }
}
@keyframes gw {
  to {
    --first: 2;
  }
}

forwardsは100%以降は100%の状態にするという設定です。この設定をデフォルトにして欲しいぐらいには自然な設定です。デフォルトは、アニメーションが終わったらアニメーションがない場合の値になります。
step-endにして、@propertyで設定した方が使いやすくていいのでは?と思う場面ですが、ここではあえて@propertyは使わず、linearで行います。これでも同じような挙動になります。(詳しい理由は16.で説明します)

--firstを確定した後は反転処理で更新していくようにするので、--initに反映させます。中心のセルはもう初期状態は決まっているので残りの0だった部分にvar(--first)を入れます。

--init: if(
  style(
    ((--w: 3) and (--h: 3)) or
    ((--w: 4) and (--h: 4))
  ): 1;
  style(
    ((--w: 3) and (--h: 4)) or
    ((--w: 4) and (--h: 3))
  ): 2;
  else: var(--first);
);

11. .lr, .rlのセルの::afterから.LR::after, .RL::afterへ4方向ずつ横流しする

セルの配置はできましたが、このままでは反転処理がないのでただ置いていくだけです。
反転処理を追加するには一手間必要です。まずは準備を行います。

反転させる方法

反転処理の方法をまずは軽く解説します。反転できるマスを特定し、できるマスなら反転のアニメーションを行い、反転させます。
反転できるマスは、各方向に対して何マス取れるかを全体に伝えて、その値を使って取れる最後のマスを特定し、そのマスが特定のマスまでの範囲にあるかどうかを使って判定します。

数値化と伝達の工夫

しかし、押しているセルに存在する各方向で取れるマス数は各セルごとのcounterの値であり、そのまま渡すことはできないです。
そのため::afterで一度counterの値を数値化し、固定のcounterに移し替えます。以降は、.WHに情報を移した際の手順とほぼ同様のことをして、全体へ伝えます。少し違う点として、8方向の情報を渡す必要があるため、.WHの場合とは違い、.board > divではなく、.lr > div, .rl > divで処理を変えなければいけないです。
また、検証により判明したことなんですが、@counter-styleで定義されたスタイルはどうやらsymbolsの1個あたり120を超えると自動で無効になるようで、120を超えると普通の数字のスタイルに戻ってしまいました。なので、1方向で最大6マス取れることを考えると、6^2=366^3=216ですが、4方向詰め込む必要があり、圧縮しても横幅だけでは足りないです(しかも::before置けるを表示する必要があるためこちらも使用できない)。
そこで今度は縦幅も使います。なので、ここでstyle.css 8.で使用したline-height: 1px;が生きてきます。こうすることで、改行(\A)するたびに縦にも情報を詰めることができるため、別のカウンターを使うことで::afterだけで4方向の6^4の情報を数値化することができるようになります。また、counter値なので扱い方もちょっと変えなければいけません。数値のようにまとめてからcounterに移すようにはできないため、1桁目用のcounterw, hと2桁目用のcounterw8, h8を分けて作る必要があります。

実装上の注意

注意ポイントなんですが、後ろに'0'もつける必要があります。
そうしておかないと、hカウンターが無効でも、0に高さは存在しているので勝手に1px高さがつけられてしまいます。なのでここでも最低1つは存在しているようにします。
さらに、先に改行のほうを書いて、後に0のカウンターにしておくことで、0が全部同じ行になるので横と縦両方で、値によって変わることがなくなります。(計算時に縦も横も-1は必要になります)
その結果このようになります。

/* afterで数値化し、ターゲットとさせて、またcounterで伝える */
.board > div::after {
  /* wだけの場合でも高さが発生してしまうため1つはつけるようにする */
  --content: if(
    style((--wW: 0) and (--hH: 0)): if(
      style(--turn: 0): counter(var(--nb), h8) counter(var(--pb), h) counter(var(--hb), w8) counter(var(--wb), w) '0';
      else: counter(var(--nw), h8) counter(var(--pw), h) counter(var(--hw), w8) counter(var(--ww), w) '0';
    );
    else: none;
  );
  content: var(--content);
  /* viewアニメーション判定のモトを設定 */
  animation: linear reverse;
  animation-name: scan1, scan2;
  animation-timeline: view(x), view(y);
  animation-range: contain cover;
}
.lr > div::after {
  counter-increment: lr1 calc(var(--data1) - 1) lr2 calc(var(--data2) - 1);
}
.rl > div::after {
  counter-increment: rl1 calc(var(--data1) - 1) rl2 calc(var(--data2) - 1);
}
/* 取得したマス数を伝える */
@counter-style w8 {
  system: symbolic;
  symbols: '00000000';
  range: 1 6;
  fallback: invalid;
}
@counter-style w {
  system: symbolic;
  symbols: '0';
  range: 1 6;
  fallback: invalid;
}
@counter-style h8 {
  system: symbolic;
  symbols: '\A\A\A\A\A\A\A\A';
  range: 1 6;
  fallback: invalid;
}
@counter-style h {
  system: symbolic;
  symbols: '\A';
  range: 1 6;
  fallback: invalid;
}
/* 横用 */
@keyframes scan1 {
  to {
    --data1: calc(85dvmin / 1px);
  }
}
/* 縦用 */
@keyframes scan2 {
  to {
    --data2: calc(85dvmin / 1px);
  }
}

12. 8方向それぞれで反転できるマス数を.LR, .RLで全体に伝える

こちらでも.WHの場合と同様にしていきます。
ついでに判定に使う際に利用しやすい形式に変更もしておきます。(8方向の変数にはその方向に取れるマス数が入っているので、+-を座標に合わせることで全部CSS変数としてマスの位置を特定できます)
.WH, .LR, .RLのデータ取得アニメーションは共通な処理のため、これらにscanクラスも付与し、.scanでまとめています。

/* viewアニメーション判定のモトを設定 */
.scan, .board > div::after {
  animation: linear reverse;
  animation-name: scan1, scan2;
  animation-timeline: view(x), view(y);
  animation-range: contain cover;
}
/* .lrに対応する.LRと.rlに対応する.RL */
.LR, .RL {
  counter-reset: var(--data1-name) var(--data2-name);
  &::after {
    /* zeroだけの場合でも高さが発生してしまうため1つはつけるようにする */
    content: counter(var(--data2-name), ln) counter(var(--data1-name), zero) '0';
  }
}
.LR {
  --data1-name: lr1;
  --data2-name: lr2;
  /* 取れるマスを取り出す */
  --d: mod(var(--data1) - 1, 8);
  --l: round(down, (var(--data1) - 1) / 8);
  --ul: mod(var(--data2) - 1, 8);
  --ld: round(down, (var(--data2) - 1) / 8);
}
.RL {
  --data1-name: rl1;
  --data2-name: rl2;
  /* 取れるマスを取り出す */
  --u: mod(var(--data1) - 1, 8);
  --r: round(down, (var(--data1) - 1) / 8);
  --dr: mod(var(--data2) - 1, 8);
  --ru: round(down, (var(--data2) - 1) / 8);
  /* 取れるマス開始位置 */
  --Wr: calc(var(--W) + var(--r));
  --Wru: calc(var(--W) + var(--ru));
  --Hru: calc(var(--H) - var(--ru));
  --Hu: calc(var(--H) - var(--u));
  --Wul: calc(var(--W) - var(--ul));
  --Hul: calc(var(--H) - var(--ul));
  --Wl: calc(var(--W) - var(--l));
  --Wld: calc(var(--W) - var(--ld));
  --Hld: calc(var(--H) + var(--ld));
  --Hd: calc(var(--H) + var(--d));
  --Wdr: calc(var(--W) + var(--dr));
  --Hdr: calc(var(--H) + var(--dr));
}

これにて反転処理を行うための準備が整いました。

13. 反転処理を行う

さあ、いよいよ反転させましょう.12で反転させるための条件の準備は全て終えました。
次に反転アニメーションを考えていきます。
--initではデフォルト(反転を無視した場合)が0(緑), 1(黒), 2(白)で変わるようになっていました。
ここで、反転とは12を交互に繰り返すことです。なので、初期値が0の場合は当然反転できません。また、2つの値を交互に繰り返せば良いので、単純に01の繰り返しで良さそうです。
なので、セルのアニメーションに反転bwも追加します。この反転アニメーションbwanimation-play-state:の条件が先ほどのものから出せる感じです。

.board > div {
  animation: gb linear 300ms forwards, gw linear 300ms forwards, bw steps(2, jump-none) 200ms infinite;
  /* --aps1は配置アニメーション用。--aps2が今回の反転アニメーション用 */
  animation-play-state: var(--aps1), var(--aps2);
  /* 反転操作をするために配置されているかを分岐させておく */
  --state: if(
    style(--init: 0): 0;
    else: calc(1 + mod(var(--init) - 1 + var(--mode), 2));
  );
}
/* 黒↔︎白 */
@keyframes bw {
  to {
    --mode: 1;
  }
}

clamp()関数を使うと最小から最大までの範囲に絞れるのでこれを利用します。以下のようにすることで、範囲内に収まっている場合のみ値が0になるような式を作ることができます。それによりif()文で判定できるようになります。(長くなるので書いていませんが、--wWr, --wWruなどは全てif文で使用するため、全て@propertyで定義しておく必要があります)

.board > div {
  /* 反転操作の判定のために設定 */
  --wWhH: calc(abs(var(--wW)) - abs(var(--hH)));
  --wWr: calc(var(--w) - clamp(var(--W), var(--w), var(--Wr)));
  --wWru: calc(var(--w) - clamp(var(--W), var(--w), var(--Wru)));
  --hHru: calc(var(--h) - clamp(var(--Hru), var(--h), var(--H)));
  --hHu: calc(var(--h) - clamp(var(--Hu), var(--h), var(--H)));
  --wWul: calc(var(--w) - clamp(var(--Wul), var(--w), var(--W)));
  --hHul: calc(var(--h) - clamp(var(--Hul), var(--h), var(--H)));
  --wWl: calc(var(--w) - clamp(var(--Wl), var(--w), var(--W)));
  --wWld: calc(var(--w) - clamp(var(--Wld), var(--w), var(--W)));
  --hHld: calc(var(--h) - clamp(var(--H), var(--h), var(--Hld)));
  --hHd: calc(var(--h) - clamp(var(--H), var(--h), var(--Hd)));
  --wWdr: calc(var(--w) - clamp(var(--W), var(--w), var(--Wdr)));
  --hHdr: calc(var(--h) - clamp(var(--H), var(--h), var(--Hdr)));
  /* 反転操作。どこも押されていなければ無効、押されているマスは無効、関係ないマスも無効 */
  --aps2: if(
    style(--scan: 0): paused;
    style((--wW: 0) and (--hH: 0)): paused;
    style(
      ((--wWr: 0) and (--hH: 0)) or
      ((--wWru: 0) and (--hHru: 0) and (--wWhH: 0)) or
      ((--wW: 0) and (--hHu: 0)) or
      ((--wWul: 0) and (--hHul: 0) and (--wWhH: 0)) or
      ((--wWl: 0) and (--hH: 0)) or
      ((--wWld: 0) and (--hHld: 0) and (--wWhH: 0)) or
      ((--wW: 0) and (--hHd: 0)) or
      ((--wWdr: 0) and (--hHdr: 0) and (--wWhH: 0))
    ): running;
    else: paused;
  );
  animation-play-state: var(--aps1), var(--aps2);
}

さあ、これにて、マスの配置反転、ターン切り替えが全て出揃いました!

14. 黒か白が置かれる前に反転の処理を実行する理由(順番が重要:反転→配置→ターン切り替え)

これまでで、時間依存のアニメーションは以下4つになりました。ここで、時間の設定がよくわからないと思ったかもしれません。これについて解説していきます。

.turn {
  animation: turn linear 750ms infinite;
}
.board > div {
  animation: gb linear 300ms forwards, gw linear 300ms forwards, bw steps(2, jump-none) 200ms infinite;
}
/* ターン切り替え */
@keyframes turn {
  to {
    --turn: 1;
  }
}
/* 緑→黒 */
@keyframes gb {
  to {
    --first: 1;
  }
}
/* 緑→白 */
@keyframes gw {
  to {
    --first: 2;
  }
}
/* 黒↔︎白 */
@keyframes bw {
  to {
    --mode: 1;
  }
}

まず、切り替わる時間は、infiniteがついているため、--turnは半分の375ms, --mode100msになります。そして--first@propertyで定義していないためこれも途中状態は取らず、半分の150msになります。
そのため、反転が最初に行われ、次に配置、最後にターン切り替えとなります。
しかし、反転させてしまうと配置条件を満たさなくなり、うまくいかないはずです。同じ時間に設定すれば良いはずですが、同じ時間に設定しても動き始めたタイミングは完全に同じではない(情報の受け渡しやアニメーションが多すぎてブラウザの処理によって結構ずれたりする)ため同じ時間に設定しても同期することはできません。そこでわざとずらします。
配置を先にやった方が各列に存在するマスは同じなため自然な感じがするんですが、それではうまくいきません。(というかできるけどだいぶ面倒になります)
その順番でやる場合、押している間に緑から黒や白に変化するのに対応させる必要があり、判定ルールを変えることはできないので、今押しているセルの状態も上に持っていき処理させる必要があります。ただでさえ多いあの8方向の判定を全部増やさないといけません。
そこで、反転を先に行います。そうすることで、修正すべきマスは今押しているマスのみになるので簡単にできます。
反転してもそのマスをまだ有効にすることができれば解決します。

それらを加味して色々試した結果、750ms, 300ms, 200msにするといい感じになったのでこの値にしました。

15. --guideを同時にアニメーションさせ、反転されても--guideの値を使用することで置く条件が解除されないようにして、なるべく安定させる

配置アニメーションに--guideという変数を追加し、反転されても置く条件が解除されないようにします。以下のようになります。ここで--guideを扱う必要があったため、わざとsteps-endではなく@propertyを使用せずにlinearを使っていました。ここで、style.css 3.が効いてきます。:hover:activeという条件ですが、.lr.rlの描画順はこの順番でした。なので、z-index:を変えておかないと、意図していない方の:hoverが発動し、:hover:activeが別要素についてしまいます。

.board > div {
  /* 触れたら変わるまで持っていく */
  --help1: calc(var(--guide) - clamp(1, var(--guide), 50));
  --pointer-events: if(
    style((--state: 0) and (--scan: 0) and (--help1: 0)): all;
    else: none;
  );
  pointer-events: var(--pointer-events);
  /* マス更新(ここでは処理せず押しているマスを全体に伝える) */
  &:hover:active {
    /* 初期値と最初のマスを区別するために1ずらしておく */
    counter-increment: var(--counter-increment) WH calc(8 * var(--h) + var(--w) + 1);
  }
}
/* 緑→黒。同期させるためにguideで修正力を持たせる */
@keyframes gb {
  to {
    --first: 1;
    --guide: 100;
  }
}
/* 緑→白。同期させるためにguideで修正力を持たせる */
@keyframes gw {
  to {
    --first: 2;
    --guide: 100;
  }
}
@property --guide {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
@property --help1 {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}

0→1, 0→2だけだったアニメーションに現在の進行度をもっと細かく書いた--guideを使用します。
もし--guideの値が0だった場合、--help1-1になります。そして、本来は::beforeで判定処理をしているため、置かれたらもう:hover:activeは発動しませんでした。しかし、少しでも押されると--guide0以外の値になります。そのため、var(--guide) - clamp(1, var(--guide), 50)によって、--guide50を超えるまで、つまり、マスが置かれるまではアニメーションが続行され、反転で条件が崩れた後でも配置までできるようになります。
(反転のアニメーションでは条件はそのままなので勝手に閉じるため再発動してしまう心配はないです)

16. ターンの切り替えも同様に、--guideを利用して、駒が置かれてもターンが切り替わるまではアニメーションするようにしてなるべく安定させる

ターンの切り替えも同じようなことを行います。--turnでは設置と違い、繰り返し行うアニメーションなので、0~5050~100の2つの区間で行う必要があります。以下のようになります。

.turn {
  /* なるべく同期させる */
  --help1: calc(var(--guide) - clamp(25, var(--guide), 50));
  --help2: calc(var(--guide) - clamp(75, var(--guide), 100));
  --pointer-events: if(
    style((--help1: 0) or (--help2: 0)): all;
    else: none;
  );
  pointer-events: var(--pointer-events);
}
/* 誤差を減らす(実際はboardの方のhover) */
.turn:hover:active {
  animation-play-state: running;
}

そこで問題になってくるのが、前回のアニメーションの誤差により次のアニメーションの誤差補正のエリアにまで入ってしまうということです。
反転アニメーションは勝手に条件が外れるので、とりあえず発動させればよく、気にする必要はありませんでした。
アニメーション時間を全体的に短くするほど誤差が大きくなりやすいが短い方が使いやすいためなるべくギリギリを攻めたいので詳しく考えます。
置くアニメーションは--guide50を超えた際に置かれます。その時点で、ターン切り替えの進行度が50%を超えてさらに次の補正エリアに入ることのないようにしなければいけません。

ターンの--guide0~誤差にある場合をスタートとし、その時点で配置も反転も進行度は0です。そこから同時にアニメーションを実行し、まず反転アニメーションを終え、その時点で配置アニメーションの--guide1~50の区間にある必要があり、さらに、配置アニメーションが終了すると、その時点でターン切り替えのアニメーションの--guide閾値~50+誤差までの区間にある必要があり、また、50+誤差50+閾値を超えていてはいけないです。

注意点

同時に動いているアニメーションが多すぎるせいか、counterを大量に使っているせいかわかりませんが、よく、突然エラーコード: 5が発生して、落ちちゃいます。また、こんなにしっかり誤差を調整したのに、初めは動きますが、どんどん変になっていきます。これ以上はあまり思いつかなかったのでここでもし動作がおかしい場合は、ブラウザをリロードすると治るし、そのまま続けてもそういう仕様ということでみてください。

まとめ

今回は、HTML, CSSだけを使ってリバーシを実装しました。ポイントとしては以下の通りです。

  1. counter-reset/counter-incrementの活用:全体の親や各セルで使い分けることで、情報の横流し、マスの判定をしました。
  2. position/view()アニメーションの活用:子要素のサイズから親要素のサイズを変更して値を取得し、色々な情報を逆流させました。
  3. 3つのアニメーションの同期:反転、配置、ターン切り替えの3つのアニメーションのタイミングを調整することで、なるべく安定したゲーム動作を実現しました。
  4. animation-play-stateの活用:animation-play-stateの通常時をpausedにすることでセルに情報を持たせて、runningで変更できるようにしました。
  5. @counter-styleの工夫range:symbols:を組み合わせて、任意のサイズでcounter値CSS変数値に変換しました。
  6. CSS変数と@propertyの活用:型定義を行うことで、if文での条件判定やアニメーション中の値の利用を可能にしました。
  7. シンプルなHTML::before::afterを活用してなるべくHTMLをシンプルにしました。
  8. style.cssとscript.cssの分離:全てをまとめず、style.cssscript.cssを分離しました。
  9. レスポンシブdvminを活用してレスポンシブにしました。

script.cssは、単なるスタイルシートではなく、ロジックを含む複雑な計算を実現するプログラミング言語に近い存在です。うまく使いこなせば、JavaScriptを使わなくてもHTMLCSSだけでリバーシを作ることができます!

これを気にどんどんscript.cssを流行らせたいと思っています!!

参考文献

@counter-style — https://developer.mozilla.org/ja/docs/Web/CSS/@counter-style
@property — https://developer.mozilla.org/ja/docs/Web/CSS/@property
counter() — https://developer.mozilla.org/ja/docs/Web/CSS/counter
counter-reset — https://developer.mozilla.org/ja/docs/Web/CSS/counter-reset
radial-gradient() — https://developer.mozilla.org/ja/docs/Web/CSS/radial-gradient
mod() — https://developer.mozilla.org/ja/docs/Web/CSS/functions/mod
round() — https://developer.mozilla.org/ja/docs/Web/CSS/functions/round
writing-mode — https://developer.mozilla.org/ja/docs/Web/CSS/writing-mode
animation-timeline / view() — https://developer.mozilla.org/ja/docs/Web/CSS/animation-timeline
animation-play-state — https://developer.mozilla.org/ja/docs/Web/CSS/animation-play-state
animation-range — https://developer.mozilla.org/ja/docs/Web/CSS/animation-range
sibling-index() — https://developer.mozilla.org/ja/docs/Web/CSS/sibling-index
if() / style()(CSS 条件関数) — https://developer.mozilla.org/ja/docs/Web/CSS
position — https://developer.mozilla.org/ja/docs/Web/CSS/position
MDN CSS リファレンス(総合) — https://developer.mozilla.org/ja/docs/Web/CSS
Scroll-driven animations — ranges(ツール) — https://scroll-driven-animations.style/tools/view-timeline/ranges

今回作成したもの

HTML / CSS のみで動作するリバーシの完成形です。
本記事内で解説している判定処理・反転処理・ターン切り替えなどの挙動を、実際に遊びながら確認できます。

単純バージョン(4方向、反転・ターン切り替え無し)

  • 公開ページ
    https://surahotoke.github.io/Reversi_4Dir_Logic
  • GitHub リポジトリ
    https://github.com/surahotoke/Reversi_4Dir_Logic
    こちらは、8方向ではなく、4方向の、しかもターン切り替えも反転処理もないバージョンです。この時点ではanimation-play-state:を自身のアニメーションで操作する必要はなかったため、sibling-index()は使用しておらず、view(x), view(y)で対処しています。また、2つを組み合わせてもないので、writing-mode:も通常のままなため、判定している方向は、, , ↘︎, ↙︎の4方向になります。また、処理方法も記事のものと(ほとんどは同じですが、)多少異なっています。

検証用ファイル群

今回の実装にあたって、仕様確認や挙動検証のために作成したコードをまとめています。

  • test1
    変数名やカウンター名などの複数の名前空間の衝突の検証(何も衝突しなかった)
  • test2
    counter + view() を組み合わせた伝搬確認(伝搬できた)
  • test3
    同一要素のいずれかのアニメーションで動くCSS変数は自身のanimation-play-state:に適用できない検証(適用できなかった)
  • test4
    アニメーション進行度はアニメーション名や回数などを更新しても常に保持される検証(保持された)
  • test5
    上流させることでanimation-play-state:でも変えれるようになる検証(変えれた)

本編では説明しきれなかった細かい挙動や、実装に至るまでの試行錯誤の記録として置いています。

jig.jp Engineers' Blog

Discussion