# 砂になる文字 — Style Reference
> 白い紙の上の黒い見出しが、触れた所から砂になって風に流れる

**Theme:** light  **Series:** PARTS 013  **World:** 架空のデータ消去サービス「Keshi」のトップ。消すという行為を、見出しそのものが砂になることで示す。

## Concept
見出し「Erase」を約4万個の粒子にサンプリングして WebGL の点として描く。静止時は普通の黒い見出しとして読める。カーソルが触れた半径の粒子だけ初速を得て、右向きの風と乱流に乗って流れ、紫に色づきながら薄れる。寿命が尽きると定位置へ戻り、語が復元する。白地・黒文字・紫1色だけで、焦点は見出し1語に置く。

## Signature Technique
文字ラスタ → 粒子サンプリング → CPU で粒子物理 → WebGL POINTS で描画。

1. **サンプリング**: offscreen canvas に Inter 800 で1語を描き、`getImageData` のアルファ>128 のピクセルを `step=2px` 間隔で拾う。位置に ±0.5px のジッタ。1440×900 で約39,000粒。
2. **状態機械**: 各粒子は `0 定位置 / 1 飛散 / 2 帰還`。
   - 0→1: カーソル中心から `radius`(文字サイズの0.23倍、26–72px)内に入ったら、放射方向×`burst`(90px/s)×(1−d/R) + カーソル速度×0.35 + 乱数 を初速にする。
   - 1: `v += (wind + turb·n1)·dt`、`v.y += (lift + gravity + 0.8·turb·n2)·dt`、`v *= drag`。n1,n2 は sin 合成の2D値ノイズ。alpha は `1 − u²·1.05`、紫への混合は `min(1, 2.2u)`(u = 経過/寿命)。
   - 1→2: 寿命(1.6–2.6s)が尽きたら、定位置の少し下に alpha 0 で置き直す。
   - 2: `pos += (home − pos)·sp`(`sp = 1 − (1 − 0.085)^(dt·60)`)、alpha を毎秒1.6回復。距離²<0.2 で 0 に戻す。
   - 抵抗も `drag^(dt·60)` でフレーム時間に合わせる。dt の上限は 0.05 秒。低速な端末でも砂が同じ距離だけ流れる。
3. **描画**: `aP`(位置) と `aS`(alpha, 紫混合率) を毎フレーム `bufferSubData`。粒ごとの乱数 `aR` は静的バッファ。頂点で `gl_PointSize = size·dpr·(0.82+0.36·aR)·(1+0.5·tint)`。フラグメントは `1−smoothstep(.36,.5,|coord−.5|)` の丸い点で、色を `ink + 0.07·aR²` だけ明るくずらし、左上から当たる光として右下側を最大10%暗くする。粒の大きさと明度が揃わないので、静止時も砂の面に見える。`SRC_ALPHA / ONE_MINUS_SRC_ALPHA` で白に合成。
4. **デモ掃引**: 操作が無い間は、粒子生成の0.2秒後から11秒おきに、仮想カーソルが語の右半分(画面幅の50%→86%)を3.2秒かけて右上がりに横切る(半径0.8倍、モバイル0.5倍)。読み込み3秒後の静止画には紫の砂の帯が写る。粒子生成は Inter 800 の読み込みだけを待つ。最初の操作で停止。
5. 粒子が全て定位置に戻り操作も無ければ rAF を止める。IntersectionObserver と visibilitychange で画面外停止。

```js
if (d2 < R2) { st[i] = 1; vel = cursorV*0.35 + dir*burst*(1-d/R) + rand; }
vel.x += (wind + turb*n1)*dt;  vel.y += (lift + gravity + 0.8*turb*n2)*dt;  vel *= drag;
alpha = 1 - u*u*1.05;  tint = min(1, 2.2*u);
```

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --white | #ffffff | 地。canvas のクリア色も同じ |
| --carbon | #1d1d1d | 粒子(静止時)、見出しの日本語、クレジット |
| --slate | #5c5c63 | 本文 |
| --ash | #9a9aa1 | ヘッダーと粒子数と操作説明に使う薄い灰色 |
| --violet | #9671ff | 飛散中の粒子の色。UI には使わない |
| --violet-mist | #f2eeff | 予備の淡い面(未使用) |
| --fog | #f6f6f6 | 予備の面(未使用) |

## Typography
- 粒子の文字: Inter 800、letter-spacing -0.04em、画面幅の78%(モバイル90%)、高さの50%上限。
- 日本語見出し: Noto Sans JP 500、`clamp(18px, 1.6vw, 22px)`、line-height 1.5。
- 本文: Noto Sans JP 400、`clamp(13px, 1.05vw, 15px)`、最大幅 36em。2文を `<br>` で分け、語の途中で折り返さないようにする。
- ラベル: JetBrains Mono 400、11–12px、letter-spacing 0.06em。粒子数は `toLocaleString('en-US')`。
- WebGL 不可時は `.fallback` の HTML テキストで同じ語を表示。

## Spacing & Layout
- hero `min-height: 100svh`、3行グリッド。canvas は背面全面。
- 余白: 横 `clamp(20px, 5vw, 72px)`、縦 `clamp(20px, 3vw, 40px)`。モバイル左右 18px。
- 語の中心は高さの46%。見出し+本文は左下、クレジットは右下。
- breakpoint 760px: ヘッダー右(粒子数)を消し、フッターを縦積み。

## Motion
- 風 150px/s(右)、上昇 −55、重力 18、乱流 220(周波数 0.006/px)、抵抗 0.975/フレーム。
- 寿命 1.6–2.6秒、戻りは一次遅れ 0.085。触れてから約3秒で語が復元する。
- カーソル速度を初速に加えるので、速くなぞるほど遠くへ飛ぶ。
- `prefers-reduced-motion: reduce`: 物理を止め、粒子を定位置に静止させた1フレームだけ描く。デモ掃引も無し。

## Components
- hero: 背面 canvas(`aria-hidden`)+前面テキスト。テキスト層は `pointer-events: none` で canvas が入力を受ける。
- header: 左に製品名、右に `particles 39,461` の実測値。
- copy: 見出し1行+本文2文。
- footer tag: `VALE / PARTS 013 / 砂になる文字` と操作説明1行(タッチ端末では文言差し替え)。

## Do / Don't
- Do: 太い書体で1語。細い字は粒子密度が足りず、静止時に読めない。
- Do: 粒子径は `step` の1.3–1.5倍にして、静止時に面として見せる。
- Do: 飛散色は1色に絞る。黒→紫の遷移だけで十分。
- Don't: 崩し半径を文字の画の太さより大きくしない。語が一度に消える。
- Don't: 風を上下左右に散らさない。方向が1つだと砂に見える。
- Don't: 背景に砂丘の画像やグラデーションを敷かない。

## References
- Riverside: 白地と近黒に、紫(#9671ff)を唯一の色として置く構造を借りた。紫を CTA に使う代わりに、飛散する粒子だけに与えた。
- Surfshark: 白い地に濃い色の塊を置いて明暗の緩急を作る考え方を借りた。塊を画像カードから粒子化した見出しに置き換え、テーマも消去サービスに寄せた。

## Usage
1. `<canvas id="gl">` を hero の背面に置き、`<link>` で Inter 800 を読み込む。
2. `<script>` ブロック全体を移植する。`document.fonts.load` 後に `sample()` が走り、粒子配列を作る。
3. `P.word` と `ctx.font` の書体を差し替える。日本語も可(Noto Sans JP 700 以上)。
4. 色は `gl.uniform3fv(U.uInk / U.uAcc, hex(...))` と `clearColor` を CSS 変数と揃える。
5. 粒子数のラベルが不要なら `#count` の行を消す。デモ掃引が不要なら `let demo = !reduce` を `false` にする。
6. WebGL が無い環境では hero に `.nogl` が付き、`.fallback` の HTML テキストが出る。語を変えたらここも揃える。

| パラメータ | 既定 | 意味 |
|---|---|---|
| step | 2 | サンプリング間隔(px)。3 にすると粒子は約半分 |
| radius | 文字サイズ×0.23(26–72) | 崩し半径(px)。`sample()` 内で自動設定 |
| burst | 90 | 崩れた瞬間の初速(px/s) |
| wind / lift | 150 / −55 | 横風と上昇(px/s²) |
| turb / turbFreq | 220 / 0.006 | 乱流の強さと空間周波数 |
| drag / gravity | 0.975 / 18 | 抵抗(毎フレーム)と重力 |
| life / lifeVar | 1.6 / 1.0 | 飛散秒数の基準と幅 |
| spring | 0.085 | 帰還の引き(0.03–0.2) |
| size | 3.0 / 2.6(モバイル) | 粒子径(px、dpr 倍される) |
| dprMax | 2 | 表示解像度の上限 |

粒子数が10万を超える場合は `step` を上げるか、物理を GPGPU に移す。
