# 形が変わる粒子 — Style Reference
> 炭色の舞台に、クリーム色の粒子が文字、球、立方体へ組み替わる

**Theme:** dark  **Series:** PARTS 031  **World:** 架空のパラメトリック製造サービス「Katachi」のトップ。同じ材料から別の形を作ることを、同じ粒子が別の形に組み替わることで示す。

## Concept
画面の中央に粒子の塊を1つだけ置く。塊は「形」の文字、球、立方体の順に組み替わり、組み替えの間だけ速い粒子がティールに色づく。粒子の位置と速度は GPU 上のテクスチャに置き、CPU は目標の切り替えと回転行列の計算だけを行う。文字は Noto Sans JP 900 の1字。背景は炭色一色で、焦点は粒子の塊に置く。

## Signature Technique
GPGPU 粒子。位置テクスチャと速度テクスチャをピンポンし、フラグメントシェーダーで更新する。

1. **テクスチャ**: 一辺 `side`(256、モバイル 160)の RGBA float テクスチャを4枚。位置 ×2(xyz + w=乱数seed)、速度 ×2。WebGL2 では `RGBA32F`(描画不可なら `RGBA16F`)、WebGL1 では `OES_texture_float`。描画できない環境では HTML の文字にフォールバック。
2. **目標形状**: CPU で一度だけ生成し float テクスチャに置く。
   - 文字: 400px canvas に1字を描き、alpha>128 のピクセルから N 個を乱択。xy を ±1.25 に正規化、z は ±0.12 の厚み(自転したとき文字の奥行きが見える)。
   - 球: Fibonacci 球面、半径 1.15、厚み 3%。
   - 立方体: 面を乱択して面上に一様、半辺 0.74。
   - 3形とも `x + 0.2y` でソートしてから格納する。粒子番号が近いほど x が近くなり、組み替え時に粒子が左右に散らばらず、帯状に移動する。
3. **速度更新**(`VEL_FS`):
   ```glsl
   float m = smoothstep(seed*.45, seed*.45+.55, uMix);   // 粒子ごとに出発時刻をずらす
   vec3 t = mix(targetA, targetB, m);
   v += (t - p) * spring * dt;                             // ばね(11)
   v += curl(p*1.1 + vec3(0, time*.15, 0)) * noise * dt;   // カールノイズ(静止 0.08、組み替え中 +0.55·sin(πmix))
   v += normalize(p - closestPointOnRay) * smoothstep(R, 0, d) * repel * dt;  // カーソルの視線から押し出す
   v *= exp(-damp * dt);                                   // 減衰(4.2)。ばね 11 > (4.2/2)² なので少しだけ行き過ぎる
   // dt は 0〜0.033 に丸める。rAF の時刻は直前の performance.now() より前になることがあり、負の dt だと減衰が増幅に反転して粒子が飛び散る
   ```
4. **位置更新**(`POS_FS`): `p += v * dt`。seed は w にそのまま書き戻す。
5. **初期位置**: 位置テクスチャの初期値は文字の目標そのもの。1フレーム目から文字の形が出る。
6. **描画**: N 個の POINTS。頂点で位置テクスチャを引き、`R = Rx(pitch)·Ry(yaw)` で回し、`xy·focal/(cam−z)` で透視投影。`gl_PointSize = size·cam/z`。点は半径 0.3〜0.5 で縁をぼかした円、alpha は最大 0.85。奥の粒子は 0.18 倍まで落とす。速度の大きさで クリーム→ティール に混ぜる。`ONE / ONE_MINUS_SRC_ALPHA` の premultiplied 合成。
7. **進行**: 保持 3.2 秒 → 組み替え 1.9 秒 を繰り返す。最初の形だけ 4.8 秒。右上のラベルをクリックすると、その形へ即座に組み替える。
8. **カーソル**: 画面座標 → 視線レイ(視点 `(0,0,cam)`)→ 回転の逆行列でワールド座標へ。粒子とレイの最近点から半径 0.42 の中で押し出す。カーソルの x/y で yaw/pitch の目標値も動く。
9. IntersectionObserver と visibilitychange で画面外停止。リサイズは canvas のみ作り直す(粒子テクスチャは保持)。

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --stage | #151515 | 地。canvas のクリア色も同じ |
| --cream | #fffbeb | 粒子(静止時)、見出し、ブランド名、クレジット |
| --mute | #b8b8b8 | 本文 |
| --ash | #7d7d78 | ヘッダーのラベル、操作説明 |
| --edge | #383835 | 予備の罫線色(未使用) |
| --teal | #00b18c | 組み替え中の速い粒子、選択中のラベル、フォーカスリング |

## Typography
- 粒子の文字: Noto Sans JP 900 の1字。細い書体だと粒子の面が途切れる。
- 日本語見出し: 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)、`text-wrap: pretty`。デスクトップで2行に収め、句点だけの行を作らない。粒子数とテクスチャ寸法は JS で実測を書き込む。
- ラベル: JetBrains Mono 400、11–12px、letter-spacing 0.06–0.08em、大文字。
- WebGL 不可時は `.fallback` の HTML テキストで同じ1字を表示。

## Spacing & Layout
- hero `min-height: 100svh`、3行グリッド(ヘッダー / 舞台 / フッター)。canvas は背面全面。
- 余白: 横 `clamp(20px, 5vw, 72px)`、縦 `clamp(20px, 3vw, 40px)`。モバイル左右 18px。
- 粒子の塊は画面中央、縦長画面では焦点距離を `min(2.4, 0.6·aspect·cam)` に縮めて幅に収め、中心を上へ 0.22(横長画面は 0.04)ずらして下のテキストと重ねない。
- breakpoint 760px: ブランド横の説明を消し、ラベルを nowrap、フッターを縦積み。

## Motion
- 初期姿勢 yaw 0.35、pitch −0.18。自動回転 0.11 rad/s。カーソル位置へ yaw ±0.5、pitch −0.18±0.3 を目標にし、毎秒 3 の一次遅れで追う。タッチ端末ではドラッグで回す(x 1.6、y 1.2 倍、pitch は ±0.8 で止める)。
- 組み替え 1.9 秒。粒子ごとの出発時刻は seed×0.45 秒ぶん散らす。組み替え中は `sin(π·mix)` でノイズを強める。
- ラベルの色は 0.6 秒で遷移。
- `prefers-reduced-motion: reduce`: rAF を回さず、内部で 90 ステップ進めてから1枚だけ描く。文字の形で静止する。

## Components
- hero: 背面 canvas(`aria-hidden`)+前面テキスト。テキスト層は `pointer-events: none`、ラベルだけ `auto`。
- header: 左にブランド名、右に `01 形 / 02 SPHERE / 03 CUBE` の読み出し。選択中はティール。
- copy: 見出し1行+本文2文(数字は実測)。
- footer tag: `VALE / PARTS 031 / 形が変わる粒子` と操作説明1行(タッチ端末では文言差し替え)。

## Do / Don't
- Do: 目標形状は必ず同じソートキーで並べる。並べないと組み替えが砂嵐になる。
- Do: 形の大きさを揃える(どれも半径 1.1–1.3 に収める)。大きさが違うと切り替えで画面からはみ出す。
- Do: 粒子色は1色、組み替え中だけ2色目を速度に応じて混ぜる。
- Don't: 球や立方体を単独で見せない。文字があって初めて「同じ粒子で別の形」が伝わる。
- Don't: 背景にグラデーションや光芒を足さない。粒子の密度差だけで奥行きを出す。
- Don't: 減衰を強くしすぎない(damp 6 以上)。行き過ぎが消えて機械的な補間に見える。

## References
- Allfeat: 炭色 #151515 に暖色クリーム #fffbeb を載せ、ティール #00b18c を1点だけ置く配色の骨格を借りた。ティールを CTA から粒子の速度表示とラベル選択に移した。
- SpaceX: 黒地に等幅・大文字・広い字間のラベルを並べ、管制画面の表示として見せる考え方を借りた。写真の代わりに粒子の塊を中央に置き、ラベルを形の切り替えスイッチにした。

## Usage
1. `<canvas id="gl">` を hero の背面に置き、Noto Sans JP 900(または任意の太い書体)を `<link>` で読み込む。
2. `<script>` ブロック全体を移植する。`document.fonts.load` 後に `build()` が目標テクスチャを作る。
3. 形を差し替える: `glyph()` / `sphere()` / `cube()` は `Float32Array(N*3)` を返す関数。独自の点群(モデルの頂点など)を同じ形式で返し、`sortX` を通して `targets` に加える。ラベル `#read span[data-i]` の数も合わせる。
4. 色は `ink` / `acc` と `clearColor` を CSS 変数と揃える。
5. 切り替えラベルが不要なら `#read` を消し、`advance()` の自動進行だけ残す。

| パラメータ | 既定 | 意味 |
|---|---|---|
| side | 256 / 160(モバイル) | 粒子テクスチャの一辺。粒子数は side² |
| word | 形 | 文字目標に使う1字 |
| hold / trans | 3.2 / 1.9 | 形を保つ秒数、組み替え秒数 |
| spring / damp | 11 / 4.2 | ばね定数と減衰。spring > (damp/2)² で行き過ぎる |
| noise / noiseIdle | 0.55 / 0.08 | 組み替え中と静止中のカールノイズ強度 |
| repel / repelR | 14 / 0.42 | カーソル反発の強さと半径(ワールド座標) |
| size | 2.0 / 2.1(モバイル) | 粒子径(px、dpr 倍) |
| cam / focal | 4.2 / 2.4 | カメラ距離と焦点距離 |
| spin | 0.11 | 自動回転(rad/s) |
| dprMax | 2 | 表示解像度の上限 |

粒子数を 100 万以上にする場合は `side` を 1024 にし、`curl()` の呼び出しを 1 回に減らすか、ノイズをテクスチャ参照に置き換える。
