# 文字を作る粒子 — Style Reference
> 白い紙に散らばった黒い粒が、打った文字の形に集まる

**Theme:** light  **Series:** PARTS 034  **World:** 架空の文字入力ツール「Moji」のトップ。打った文字がそのまま粒子の形になることで、入力と表示の距離が無いことを見せる。

## Concept
白地に黒い粒子だけ。起動時は画面全体に散らばっていて、1秒ほどで入力欄の文字の形に集まる。入力欄に別の文字を打つと、粒子は一度四方に散り、新しい文字へ集まり直す。動いている粒子だけ橙に色づき、止まると黒に戻る。カーソルが近づくと粒子はよけ、離れると戻る。焦点は粒子の文字1語。左下に見出しと本文と入力欄。

## Signature Technique
GPGPU 粒子。目標点群へのばね力を GPU で積分する。

1. **テクスチャ**: 一辺 `side`(128、モバイル 96)の RGBA float を 5 枚。位置 2 枚(xy + w=seed)と速度 2 枚と目標 1 枚。座標は画面高さ=1、幅=aspect。WebGL2 は `RGBA32F`(不可なら `RGBA16F`)、WebGL1 は `OES_texture_float`。
2. **目標点群**(`sampleWord`): offscreen canvas に Noto Sans JP 900 で文字を描き、画面幅の 72%(モバイル 88%)・高さの 50% 以内に収める。alpha>128 のピクセルを `step = 文字サイズ/40`(モバイル /34)px 間隔で拾い、N 個の粒子に乱択で割り当てる(±step/2 のジッタ)。文字が変わるたびに目標テクスチャだけ上書きする。
3. **速度更新**(`VEL_FS`):
   ```glsl
   v += (target - p) * spring * dt;                 // ばね(38)
   v += dir(hash(seed, floor(t*3))) * jitter;        // 静止時の揺らぎ(0.012)、1/3 秒ごとに向きが変わる
   v += (p - cursor)/r * smoothstep(R, 0, r) * repel; // カーソル反発(2.4、半径 0.11)
   if (burst > 0) v += dir(hash(seed)) * burst * (0.4 + hash);   // 文字変更の 1 フレームだけ散る(1.6)
   v *= exp(-damp * dt);                             // 減衰(7.5)。spring 38 > (7.5/2)² なので少し行き過ぎて止まる
   ```
4. **位置更新**(`POS_FS`): `p += v·dt`。seed は w に書き戻す。
5. **描画**: N 個の POINTS。`gl_PointSize = size·dpr`(2.4、モバイル 2.2)、速さ 0.25→1.6 で黒→橙に混ぜ、径も 1.5 倍まで大きくする。点の縁は `smoothstep(.35,.5,d)` でぼかし、不透明度 0.92。`ONE / ONE_MINUS_SRC_ALPHA` の premultiplied 合成で白に重ねる。
6. **入力**: `<input maxlength="8">` の `input` イベントを 220ms デバウンスし、`setWord(text, true)` で目標を差し替えて散らす。
7. 起動時は 40 フレーム進めてから表示(7割ほど集まった状態。遅い端末でも読み込み3秒後に文字が読める)。rAF の dt は 0〜0.033 に丸める。rAF の時刻は start() 直前の performance.now() より前になることがあり、負の dt だと減衰が増幅に反転して粒子が画面外へ飛び散る。IntersectionObserver と visibilitychange で画面外停止。リサイズで目標を取り直す。

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --white | #ffffff | 地。canvas のクリア色も同じ |
| --snow | #f5f5f7 | 入力欄の面 |
| --silver | #d0d0d3 | 入力欄の枠 |
| --pewter | #ababb0 | ヘッダーのラベル、入力欄のラベル、操作説明 |
| --smoke | #6e6e73 | 本文 |
| --ink | #101010 | 粒子(静止時)と見出しとブランド名と入力文字 |
| --amber | #f06413 | 動いている粒子、キャレット、フォーカスリング(28%) |

## Typography
- 粒子の文字: Noto Sans JP 900。入力欄の文字をそのまま使う。細い書体は粒子が途切れる。
- 日本語見出し: Noto Sans JP 500、`clamp(20px, 1.8vw, 26px)`、letter-spacing −0.01em、line-height 1.45。
- 本文: Noto Sans JP 400、`clamp(13px, 1.05vw, 15px)`、最大幅 30em(761px以上は 34em)。デスクトップでは1文目の後で改行する `br.d` を表示し、語の途中で行が割れないようにする。
- 入力欄: Noto Sans JP 500、18px、padding 8px 12px、角丸 4px。
- ラベル: JetBrains Mono 400、11–12px、letter-spacing 0.06–0.08em。粒子数は JS で実測を書き込む。
- WebGL 不可時は `.fallback` の HTML テキストで既定の文字を表示。

## Spacing & Layout
- hero `min-height: 100svh`、3行グリッド。canvas は背面全面。
- 余白: 横 `clamp(20px, 5vw, 72px)`、縦 `clamp(20px, 3vw, 40px)`。モバイル左右 18px。
- 文字の中心は高さの 46%(縦長画面では 40%)。見出し・本文・入力欄は左下、クレジットは右下。
- 入力欄は最大幅 22em、ラベルと 12px の間隔。モバイルでは幅いっぱい。
- breakpoint 760px: ブランド横の説明を消し、フッターを縦積み。

## Motion
- 集まる時間: ばね 38 / 減衰 7.5 で約 1 秒。行き過ぎは 1 回だけ。
- 文字変更: 1 フレームの初速 1.6(画面高さ/秒)で散り、約 1.5 秒で新しい形になる。
- カーソル: 半径 0.11 の中で押し出し、離れると戻る。タッチはなぞっている間だけ。
- 揺らぎ 0.012 で静止時も粒子がわずかに震える。
- `prefers-reduced-motion: reduce`: rAF を回さず、150 ステップ進めて集まった状態を1枚描く。文字変更時も同じ(散る動きは見せない)。

## Components
- hero: 背面 canvas(`aria-hidden`)+前面テキスト。テキスト層は `pointer-events: none`、入力欄だけ `auto`。
- header: 左にブランド名、右に `16,384 particles`。
- copy: 見出し1行+本文2文+入力欄(`label` 付き、`aria-label` 付き、`focus-visible` で枠が黒、橙のリング)。
- footer tag: `VALE / PARTS 034 / 文字を作る粒子` と操作説明1行(タッチ端末では文言差し替え)。

## Do / Don't
- Do: 粒子数は文字のピクセル数の 1–2 倍に。少ないと穴だらけ、多いと面になって粒子に見えない。
- Do: 目標の割り当ては乱択にする。順番に割り当てると集まるときに縞が出る。
- Do: 色は動いている粒子だけに付ける。止まった文字は黒1色で読ませる。
- Don't: 減衰を弱くしない(damp 5 未満)。粒子が文字の周りで振動し続ける。
- Don't: 散る初速を 3 以上にしない。画面外へ出て、戻るまで文字が欠ける。
- Don't: 入力欄を隠さない。打てることがこのパーツの主役。

## References
- Paste: 白と近黒の高コントラストに、暖色の橙を1点だけ置く構造を借りた。橙をロゴから、動いている粒子とキャレットに移した。
- Avoice: 白い作業面の上で、入力や選択の瞬間だけ鮮やかな色が出る考え方を借りた。ライムの代わりに Paste の橙で統一し、入力欄のフォーカスに使った。

## Usage
1. `<canvas id="gl">` を hero の背面に置き、Noto Sans JP 900(または任意の太い書体)を `<link>` で読み込む。
2. `<input id="word">` を置く。入力欄が不要なら、IIFE の中から `setWord('任意の文字', true)` を任意のタイミングで呼ぶ(スクロールやボタンでも可)。外から呼ぶ場合は IIFE 末尾で `window.setWord = setWord` を足す。
3. `<script>` ブロック全体を移植する。`document.fonts.load` 後に `boot()` が走る。canvas の大きさは `#hero` の clientWidth/Height から取るので、親要素の id を合わせるか `hero` の取得先を変える。
4. 書体は `sampleWord()` の `ctx.font` で差し替える。色は `ink` / `acc` と `clearColor` を CSS 変数と揃える。暗い地にする場合は `clearColor` と `ink` を入れ替える。
5. 粒子数の読み出しが不要なら `#n` の行を消す。

| パラメータ | 既定 | 意味 |
|---|---|---|
| side | 128 / 96(モバイル) | 粒子テクスチャの一辺。粒子数は side² |
| fill / maxH | 0.72 / 0.5 | 文字の幅と高さの上限(画面比) |
| spring / damp | 38 / 7.5 | ばね定数と減衰 |
| jitter | 0.012 | 静止時の揺らぎ |
| repel / repelR | 2.4 / 0.11 | カーソル反発の強さと半径 |
| burst | 1.6 | 文字変更時に散る初速 |
| size | 2.4 / 2.2(モバイル) | 粒子径(px、dpr 倍) |
| hot | [0.25, 1.6] | 速さ→橙の範囲(画面高さ/秒) |
| dprMax | 2 | 表示解像度の上限 |

文字数を 8 より増やす場合は `fill` を上げ、`side` を 192 にする。
