# 立体マーキー — Style Reference
> 黒いステージの上で、白と黄の帯が三段で回り続ける

**Theme:** dark  **Series:** PARTS 014  **World:** 架空のライブ会場「Obi」のトップ。今夜の開場時刻・開演時刻・会場名を、回り続ける帯で見せる。

## Concept
告知文を3本の円筒の帯に貼り、それぞれ別の速さと向きで回す。手前の文字は大きく、奥は小さく暗く沈むので、画面に奥行きが生まれる。中央の1本だけバター黄にして、会場名を焦点にする。装飾は帯だけで、背景は無地の黒。ドラッグで回り、スクロールで加速して、惰性で元の速さに戻る。

## Signature Technique
Three.js の `CylinderGeometry`(側面のみ)に、2D canvas に描いたテキストを `CanvasTexture` として貼る。

- **テクスチャ**: 4096×180px。円周 2π×2.6 ≒ 16.3 と帯高さ 0.72 の比(約22.7:1)に合わせて、文字が横に伸びないようにしている。文言+`   ·   `を1単位として整数回ぴったり繰り返すよう、`ctx.scale(texW/(unitWidth·n), 1)` で横だけ伸縮し、継ぎ目を消す。Inter 900、文字サイズは高さの66%。
- **ジオメトリ**: `CylinderGeometry(2.6, 2.6, 0.72, 128, 1, true)` を3段。間隔 0.16。グループ全体を X 軸に −0.16rad 傾け、わずかに下(y=−0.05)に置き、見出しとヘッダーの間の中心に帯の束を合わせる。
- **奥行きの暗さ**: `MeshBasicMaterial` の `onBeforeCompile` で、ワールド法線とカメラ方向の内積 `vFace` を varying で渡し、`gl_FragColor.rgb *= mix(0.06, 1.0, smoothstep(-1.0, 0.65, vFace))`。裏側の文字は鏡文字のまま暗く残り、立体として読める。
- **艶と縁の光**: 同じ `onBeforeCompile` で、左上のキーライト `L = normalize(−0.55, 0.45, 1.0)` とカメラ方向のハーフベクトルから Blinn-Phong の艶を足す。`spec = pow(n·h, 22)·0.16 + pow(n·h, 160)·0.22`。広い艶と細い艶を重ねると、帯がサテンのリボンの質感になる。さらに `uv.y` の上下 3.5% に `0.28` の縁の反射を足し、帯の厚みを見せる。艶と縁は表側(vFace > 0)だけ。色は `#fffaed` 寄りの白。
- **カメラ**: fov 30、`lookAt(0, 0.1, 0)`。距離はアスペクト比で 10.8 / 11.4 / 12.2(aspect ≥1.3 / 0.8–1.3 / <0.8)。縦長では帯が左右で切れるが、帯としての読みは保たれる。
- **回転**: 各帯 `rotation.y += (speed + extra·sign(speed))·dt`。初期角は帯ごとに 0.9rad ずつずらす。speed = 0.16 / −0.11 / 0.13 rad/s。`extra` はドラッグ量×0.0045、スクロール量×0.0025 で増え、毎フレーム 0.94 倍で減衰。
- rAF 1本。dt は 0〜0.05 秒に丸める。IntersectionObserver と visibilitychange で画面外は停止。`document.fonts.load('900 100px Inter')` の後にテクスチャを作り直す。

下のコードは奥行きの暗さだけを抜き出した要点。実装では同じ置換の中で `vN`(法線)、`vV`(視線)、`vY`(uv.y)も渡し、艶と縁の光を足している。

```js
mat.onBeforeCompile = sh => {
  sh.vertexShader = sh.vertexShader
    .replace('#include <common>', '#include <common>\nvarying float vFace;')
    .replace('#include <begin_vertex>', '#include <begin_vertex>\nvec3 wn=normalize(mat3(modelMatrix)*normal); vec4 wp=modelMatrix*vec4(position,1.); vFace=dot(wn, normalize(cameraPosition-wp.xyz));');
  sh.fragmentShader = sh.fragmentShader
    .replace('#include <common>', '#include <common>\nvarying float vFace;')
    .replace('#include <dithering_fragment>', '#include <dithering_fragment>\ngl_FragColor.rgb *= mix(0.06, 1.0, smoothstep(-1.0, 0.65, vFace));');
};
```

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --void | #000000 | 画面の地。renderer の clear color も同じ |
| --carbon | #111111 | CSS 上の予備。黒い帯の面はテクスチャ背景 `#0e0e0e` で、地よりわずかに明るく円筒の輪郭が出る |
| --paper | #ffffff | 黒い帯の文字、日本語見出し、ヘッダーの会場名 |
| --butter | #f5e78d | 中央の帯の面と、クレジットの1行。画面で唯一の色 |
| --smoke | #454545 | 予備(罫線が要るとき) |
| --ash | #8a8a8a | 本文とヘッダー補助と操作説明に使う灰色 |

## Typography
- 帯の文字: Inter 900、大文字、letter-spacing −0.03em。canvas 上でラスタライズ。
- 日本語見出し: Noto Sans JP 500、`clamp(18px, 1.6vw, 22px)`、line-height 1.5。
- 本文: Noto Sans JP 400、`clamp(13px, 1.05vw, 15px)`、最大幅 34em。会場の事実と仕組みの説明を `<br>` で2行に分け、語の途中で折り返さない。
- ラベル: JetBrains Mono 400、11–12px、letter-spacing 0.06em。
- WebGL 不可時は `.fallback` に同じ文言を1行の HTML テキストで表示。

## Spacing & Layout
- hero `min-height: 100svh`、3行グリッド。canvas は背面全面、`touch-action: pan-y` で縦スクロールを妨げない。
- 余白: 横 `clamp(20px, 5vw, 72px)`、縦 `clamp(20px, 3vw, 40px)`。モバイル左右 18px。
- 帯の束はグループ y=−0.05、カメラ注視点 y=0.1 で、画面の縦中央(ヘッダーと見出しの間)に来る。見出しと本文は左下、クレジットは右下。
- breakpoint 760px: ヘッダー右を消し、フッターを縦積み。

## Motion
- 定常回転: 0.16 / −0.11 / 0.13 rad/s(1周 約39 / 57 / 48秒)。隣同士が逆向き。
- ドラッグ: 横移動 1px につき 0.0045 rad/s を加算。スクロール 1px につき 0.0025。ホイールは deltaY を ±80 に丸めて 0.6 倍。
- 減衰: 0.94^(60·dt)。約1秒で惰性が消える。
- `prefers-reduced-motion: reduce`: 定常回転を 0 にし、初期位置(各帯 0.9rad ずつずらす)で静止。ドラッグとスクロールでのみ回り、止まったら rAF を止める。

## Components
- hero: 背面 canvas(`aria-hidden`、`cursor: grab`、ドラッグ中 `grabbing`)+前面テキスト。
- header: 左に会場名、右に技法名。
- copy: 見出し1行+本文2文。時刻と会場名は帯の文言と一致させる。
- footer tag: `VALE / PARTS 014 / 立体マーキー`(黄)と操作説明1行(タッチ端末では差し替え)。

## Do / Don't
- Do: 帯は3本まで、色付きは1本だけ。
- Do: 文言は短い名詞句にする。1周に3〜4回繰り返せる長さが読みやすい。
- Do: 背景は無地。帯の明暗だけで奥行きを出す。
- Don't: 帯ごとに書体や色を変えない。
- Don't: 艶の係数(0.16 / 0.22)を上げない。文字の上で白く飛び、帯の文言が読めなくなる。
- Don't: 回転を速くしない。1周30秒より速いと文字が追えない。

## References
- Guglieri: 黒・白・灰だけで階層を作り、重い display 書体を主役にする考え方を借りた。平面のギャラリーを円筒に巻き、文字の大小を遠近で作った。
- Bedouin's Daughter: 黒い面に対してバター黄を編集的に通す配色と、大文字の告知マーキーという型を借りた。黄を全体に通す代わりに3本のうち1本に絞り、会場名の焦点にした。

## Usage
1. importmap で three@0.170.0 を読み込み、`<canvas id="gl">` を hero の背面に置く。`<link>` で Inter 900 を読み込む。
2. `<script type="module">` ブロック全体を移植する。
3. `P.lines` に文言を入れる(帯の本数は配列長で決まる)。`P.accent` で黄にする帯を選ぶ。
4. 色を変えるときは `makeTexture(line, fg, bg)` の呼び出し2か所(初回と、フォント読込後の描き直し)の引数と CSS 変数を揃える。
5. 入力が不要なら pointer / scroll / wheel のリスナーを消す。

| パラメータ | 既定 | 意味 |
|---|---|---|
| lines | 3本 | 帯の文言(上から) |
| speeds | 0.16 / −0.11 / 0.13 | 各帯の角速度(rad/s)。負は逆回転 |
| radius / bandH / gap | 2.6 / 0.72 / 0.16 | 円筒半径、帯の高さ、間隔(scene 単位) |
| tilt | −0.16 | 円筒の傾き(rad) |
| texW / texH | 4096 / 180 | テクスチャ解像度。比は 2πr : bandH に合わせる |
| accent | 1 | 黄にする帯の index |
| dprMax | 2 | 表示解像度の上限 |
| scrollGain / dragGain | 0.0025 / 0.0045 | 入力1pxあたりの加速 |
| friction | 0.94 | 惰性の減衰(毎フレーム) |

帯の本数や高さを変えたら、`camera.position.z` の値(resize 内)を見直す。
