🌟星占い10万年でブラウザの限界を越える🌟

ご無沙汰してます。ここはクソアプリアドベントカレンダーの12日目です。
今年も無事クソアプリを生み出すことができました。
皆さんの来年がより良いものになるよう、今日は10万年分の星占いをお送りします:cat:

### 過去ログ
- 2024 星占い10万年 👈いまここ
- 2023 [🎉🎉最高にイカしたバウンディングボックスを紹介するぜ🎉🎉](https://qiita.com/yuneco/items/4ef41286136493d76f9d)
- 2022 [【ChatGPTと作る】あしのさきの動物パンプリン占い【クソアプリ】](https://qiita.com/yuneco/items/ade09021f18afbaded88)
- 2021 [😡webのスクロールでふわっと出てくるやつ絶対粉砕するマン【クソアプリ】](https://qiita.com/yuneco/items/24a209cb14661b8a7a20)
- 2020 [Vue.jsと物理演算とElectronで仕事中にデスクトップでお寿司をつまめるようになったのでソースと解説【クソアプリ】](https://qiita.com/yuneco/items/1fa72413ab011d1c0ac5)

## できたもの

アプリ：
https://yuneco.github.io/perpetual-astrology/

ソースコード：
https://github.com/yuneco/perpetual-astrology

## :sparkles: 解説と見どころ :sparkles:

名前と星座を入れると…
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/9aba334c-8aa9-ec49-ca11-7d45d2c06e0d.png width="50%">


秒で10万年分、毎日の運勢を占って勿体ぶらずに全部表示します
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/36373a9f-0fc1-ce98-1cac-f4de1816ef28.png width="50%">


好きなところまでスクロールしてタップしたら…
![scrollandtweet.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/ec3a7556-bea9-db27-329c-30341178e41b.gif)


X(Twitter)でシェアしましょう
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">できた。我ながら良いクソアプリだ<br><br>nekochanさん♈️牡羊座<br>99999年12月29日（水）の運勢<br>🔮運勢:★★★★<br>🔮ラッキーカラー:<a href="https://twitter.com/hashtag/fd3289?src=hash&amp;ref_src=twsrc%5Etfw">#fd3289</a><br>🔮ラッキーサイゼ:🧂エクストラ・バージン・オリーブオイル (500ml) + 🥔カリッとポテト + 🍚ラージライス (1680円) <a href="https://t.co/OsZj2XuvfN">https://t.co/OsZj2XuvfN</a></p>&mdash; ゆき (@yuneco) <a href="https://twitter.com/yuneco/status/1866898902068424887?ref_src=twsrc%5Etfw">December 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

リンクがつくので10万年分の運勢を全部インターネッツに大公開できます:blush:

## :sparkles: 使った技術 :sparkles:

今回は稀に見るシンプルさです→[package.json](https://github.com/yuneco/perpetual-astrology/blob/main/package.json)

ざっくりいうと、
- React 19
- Kuma UI

だけですね。環境周りは普通にVite + TSです。

## :sparkles: よくわかる解説 :sparkles:

もう大体の人はわかってると思うけど、**やってることはただの仮想スクロール**です。
10万年＝36500000日分のDOMを律儀に作ると当然まともに動かないので、画面に見えている範囲だけDOMを作ろうってやつです。

ということで10万年星占いを作るのに必要なのは、
- 36500000行の巨大リストを仮想スクロールする実装
- スクロール位置に合わせた星占いを表示する実装

の2つだけ、ということになります。簡単ですね:wink:

### ブラウザのスクロールは星占いには狭すぎる

そう、仮想スクロール自体は難しくないんですよ[^easy]
数年前に流行って散々擦られたので良くできたライブラリも多そうです（こういうのとか：[react-infinite-scroller](https://github.com/danbovey/react-infinite-scroller#readme)）

[^easy]: サーバ負荷含めた実運用とか快適なUX設計とかが簡単とは言ってないので注意

ただし、これはリストのサイズが常識的な範囲の場合です。**実は現代の主要ブラウザが扱えるDOMのスクロール上限は:star2:星の世界:star2:から見ると結構小さい**んです。

詳しいことは[バーチャルスクロールの限界を突破する](https://zenn.dev/kohii/articles/50e0bf572aac0b)を参照いただくとして、
Chromeの限界は`16777216px`。1行（1日分）の高さが50pxだとすると、これって大体919年分。たった一千年の星占いすら表示できない有様です:poop:

ちなみに10万年分に必要な高さは`18億2500万px`です

### 限界を越える戦略

前出の解説記事では[スクロールバーとスクロール機能を自作](https://zenn.dev/kohii/articles/50e0bf572aac0b#%E9%99%90%E7%95%8C%E3%82%92%E8%B6%85%E3%81%88%E3%82%8B%E5%AE%9F%E8%A3%85)していましたが、今回我々が作るのはクソアプリです。そんなクソめんどくさいことやりたくないですよね？

ちょっと真面目な話としては、きちんと動作するスクロールの実装のためには
- マウス・ホイール・タッチ等の各種入力の対応
- スペース・PageUp等、各種キー操作の対応
- バーを直接ドラッグした場合や、トラック部分をクリックした場合の対応
- ネイティブと遜色ない見た目と性能
- アクセシビリティのケア

…などなど考えないといけないことがものすごく多いです。相応の覚悟がない限りやるべきではありません。今回は手抜きのため、できる限り標準のスクロールを使って限界を超えます

というわけで、下図は10万年星占いのDOMをちょっとみやすく調整したものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/a0515535-2147-fc34-26e4-2fe2a9bc5589.png)


実は画面右に出ていたスクロールバーは本体のリストとは別物です。<font color=red>この細いスクロールエリア（赤）を「ミニマップ」</font>とでも呼びましょう。
<font color=blue>メインのスクロールエリア（青）</font>は画面全体に配置した上でスクロールバーを隠します。こっちを<font color=blue>「メインエリア」</font>と呼びましょう。
肝心の<font color=green>星占いのコンテンツ（緑）</font>は2つのスクロールとは別に`position: sticky`で画面上に固定します。

まず、<font color=red>ミニマップ（赤）</font>からです。
ミニマップは実際には画面の10倍程度の高さしかない普通のスクロールバーです。現在位置を示したり、直接バーをドラッグしたりして10万年をスクロールできます。わかりやすく高さを1800pxとすると、このスクロールバーの値x100000が実際のコンテンツの仮想スクロール位置になります。
![minimap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/a5d84042-99b8-7716-4615-153af70e73d6.gif)

次に<font color=blue>メインのスクロールエリア（青）</font>です。
こっちはもう少し高さを確保していますが、それでも20000px程度のスクロールバーです。初期位置は真ん中になっていて、上下どちらにでもスクロールできます。
ただ、これだけだと上下に10000pxしかスクロールできません。
これを誤魔化すために、隙を見てスクロールをリセットしています。
![mainlist.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/8111837e-127d-7891-7631-fa8215926427.gif)

GIFをよくみていると、スクロールの合間合間で**スクロールボックスの位置が中央にリセット**されていることがわかると思います。リセットした際にスクロールされていた量は<font color=red>ミニマップ（赤）</font>のスクロール量に加算します。

最後の<font color=green>コンテンツエリア（緑）</font>は特に何もありません。
この領域はスクロールせず、2つのスクロールバーから算出した仮想スクロール量に応じて日付と占い内容を書き換えます。

完璧とは言い難いですが、標準のスクロールUIを使いながらでもブラウザの限界を突破できましたね :tada: 

### 10万年分の星占いを計算する

もうここまでで面白いことは大体書き切っちゃったのですが、一応星占いアプリなんでコンテンツの方の話もしましょう。いうまでもなくこの占いはただの乱数なのですが、JavaScript標準の`Math.random()`を使うわけにはいきません。

同じ名前・星座を入れているのに同じ日の運勢が毎回違ったり、スクロールして戻ってきたら運勢が変わっている、なんてことになったら興醒めです。

```ts
運勢 = f(名前, 星座, 日付)
```

のような純粋な関数が必要です。
まあ、要は乱数ではなくハッシュですね。

ブラウザ標準で使えるハッシュ関数としては[SubtleCrypto: digest()](https://developer.mozilla.org/ja/docs/Web/API/SubtleCrypto/digest)のような便利なものもあるのですが、クソアプリで使うには少々真面目すぎるのと、インターフェースが非同期関数で面倒なのでもっと手抜きします。

今回はChatGPTに[メルセンヌ・ツイスターの擬似乱数列生成器を書いてもらいました](https://github.com/yuneco/perpetual-astrology/blob/main/src/contents/logics/MersenneTwister.ts)。
多少の効率化はしていますが、基本は`名前, 星座, 日付`を元に作ったシード値をメルセンヌ・ツイスターにぶちこんで疑似乱数を生成して使い捨てています。
メルセンヌ・ツイスターの無駄遣い。

ハッシュ値が得られればあとはごにょごにょ加工して⭐️の数にしたり、ラッキーカラーにしたり、[サイゼリアのメニューにしたり](https://github.com/yuneco/perpetual-astrology/blob/main/src/contents/logics/getMenu.ts)、[ラッキーUUIDにしたり](https://github.com/yuneco/perpetual-astrology/blob/main/src/contents/logics/getUuid.ts)やりたい放題です。

## :sparkles: 技術的な感想 :sparkles:

### React 19

- 正直あんまりわかってないけど普通に使えた。別にReact 18がわかっていたわけではないのでそういう意味では変わらず。ありがたいことです
- `ref`にクリーンアップがついたので使ってみた。個人的にはReactのイベントハンドラ周りの扱いに辟易していたので[ここら辺のスクロールイベントの処理を（フックじゃない）ただの関数](https://github.com/yuneco/perpetual-astrology/blob/main/src/infScroll/watchInfScrollTop.tsx)に書いて、[refでDOMに紐づける](https://github.com/yuneco/perpetual-astrology/blob/b6d229460f9551e5a8a24a4b3b46b2b5bd3ab902/src/infScroll/InfScroller.tsx#L48-L63)書き方を試してみた

### :bear: Kuma UI 

- 仕事で使うには時期尚早だと思ってあんまり触らずに放置してしまってたので、今回ノープラン＆雰囲気で使ってみた
- emotionの後継だって考えるのをやめて、KumaはKumaとして仲良くなろうと思ったら結構楽しくていい感じ
- ただ、いかんせん未対応のプロパティや機能が多いので、謎の縛りゲー感が強い
  - `@keyframe`がKumaの機能では定義できない
  - `transform`の個別変形プロパティ（`transform: scale(2)`ではなく`scale: 2`みたいに書ける方）が使えない
  - ベンダープレフィックスどうすればいいんだろ？
  - ブレークポイント以外のメディアクエリって使えない、よね？
- なのでやっぱり仕事で使うにはしんどいけど、個人開発で触る分には楽しいから期待してる

## 来年もよろしくお願いします！

今年クソアプリくらいしか作ってなくてお恥ずかしい限りですが来年もよろしくお願いします:wave: