# 文字の世界 — Style Reference
> a terminal-black room where a rotating island and a tsubo jar are drawn entirely in kanji, light faces in dense characters and shadow in single strokes, and the body copy floats inside the thinned field.

**Theme:** dark  **Series:** PLAY 008  **World:** 架空の展覧会「活字彫刻展」の案内ページ。立体を漢字の濃淡で描く。

## Concept
Everything on the canvas is type. A real 3D scene (an island terrain and a lathe-turned tsubo jar) is rendered to a tiny off-screen buffer, and every texel of that buffer becomes one kanji chosen by brightness from a 16-step ramp that runs from a blank through 一 and 口 up to 鬱 and 龗. The viewer drags to rotate and the characters swap live as the light moves across the form. Each section changes the object and the cell size: the hero shows the tsubo at 13 px cells with a 0.14 rad tilt so the mouth reads, lit hard from the upper left so the lit belly runs to 龗 and the shadow side to a blank; it turns on its own at 0.3 rad/s, the gradation section zooms the cells to 24 px around the same jar so the ramp itself becomes legible, the terrain returns at 11 px for the drag section. The copy is a plain white column on the left; the glyph field is thinned to near-blank inside the copy's rectangle so the words sit in a quiet pocket of the picture. One hairline of ember orange marks the eyebrows and the drag hint; nothing else has colour.

## Signature Technique
Three.js r170, two render passes, one requestAnimationFrame.

- **Glyph atlas with measured coverage.** 24 candidate kanji are drawn one by one into a 64 px canvas in Noto Sans JP 400 and their alpha sum is read back as ink coverage. They are sorted by coverage and 16 are picked at even coverage intervals, so the ramp is linear in blackness and not in stroke count (two glyphs with the same stroke count can differ a lot in ink). The picked glyphs are drawn into a 1024 x 64 atlas (`CanvasTexture`, linear filtering, no mipmaps) and the same list fills the DOM ramp strip in the gradation section. The atlas is rebuilt once the web font finishes loading.
- **Scene at grid resolution.** The grid is `cols = ceil(W / cell)`, `rows = ceil(H / cell)`. The scene renders into a `WebGLRenderTarget` of exactly 2 x that size with linear filtering, so one bilinear fetch at a cell centre is a 4-texel average. Nothing is wasted on pixels that would be thrown away.
- **Post shader.** For each fragment: `cell = floor(uv * grid)`, `f = fract(uv * grid)`, luminance from the scene texel at the cell centre, `lum = smoothstep(0.02, 0.92, lum)` followed by one more smoothstep for an S-curve, so the mid-greys spread across the whole ramp, index `floor(lum * 15.999)`, atlas sample at `((index + f.x) / 16, f.y)`. Glyph brightness is `0.34 + 0.66 * lum` so the darkest glyphs are grey and the brightest white. A rectangle uniform (the current section's copy block, read from `getBoundingClientRect` each frame) multiplies luminance by 0.08 with a 90 px soft edge. A hashed 1.5 percent of cells show a faint 丶 as ambient noise so the void has texture.
- **Objects.** Terrain: 170 x 170 plane displaced by 6-octave value noise, island mask `1 - r^3`, heights above sea level raised to 1.25 and scaled by 2.0 for tall ridges, flat cylinder base. The terrain has its own material (`#E8E8E8`, roughness 0.85, no emissive) so slopes facing away from the key fall all the way to thin strokes and the peak stands out against the field. Tsubo: `LatheGeometry` from a 21 point profile (narrow foot, high wide shoulder at r 1.94, short neck, rolled lip), a raised torus band on the shoulder and two small torus lugs on the shoulder so the turn is visible on a body of revolution. The tsubo uses a white `MeshStandardMaterial` (roughness 0.75, emissive #2A2A2A so the shadow side still prints as thin strokes and the whole silhouette survives) under a 2.7 directional key from the upper front left (-2.6, 3, 5), a 0.3 hemisphere fill and a 1.1 rim light from the back right. On mobile the terrain drops to 110 segments.
- **Scroll keyframes.** `{obj, cell, tilt}` per section: tsubo 13 / 0.14, tsubo 13 / 0, tsubo 24 / 0.12, terrain 13 / 0.5, tsubo 16 / 0.08. Blend is `smoothstep(0.25, 0.75, frac)`; objects crossfade by scale (weight 0 to 1) and sink 1.5 units while hidden. Cell size changes re-allocate the render target only when it moves more than 0.6 px.
- **Drag.** Pointer capture on the canvas, `rotY += dx * 0.008`, velocity kept from the last move and decayed at `exp(-2.2 dt)`; idle spin 0.3 rad/s resumes 2.5 s after release. Arrow keys rotate by 0.25 rad. `touch-action: pan-y` keeps vertical scrolling on phones.
- **Reduced motion.** No idle spin, no inertia; drag still rotates; crossfades snap.

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --bg | #0b0b0b | page and scene clear colour |
| --ink | #f1f0ec | headings, brightest glyphs, button border |
| --mute | #8a8a86 | body copy, eyebrows, mono labels, dark glyphs |
| --hair | rgba(241,240,236,.14) | pill borders, ramp cell borders, footer rule |
| --ember | #cc6437 | eyebrow rule, drag hint dot, focus ring, hover border |
| glyph white | #f1f0ec at 42 to 100 percent | the sixteen shades the shader produces |

A monochrome system with one hairline of ember, after Ciridae. The ember is never a fill.

## Typography
- **Noto Sans JP** 300 / 400 / 500. h1 clamp(56px, 8vw, 116px) weight 400, letter-spacing -0.03em, line-height 1.05. h2 clamp(30px, 3.4vw, 48px) weight 400, -0.02em. Body clamp(15px, 1.1vw, 17px) weight 300, line-height 1.9, max 38ch. The same family draws the glyph atlas, so the picture and the copy are literally the same typeface at two scales.
- **IBM Plex Mono** 400 / 500 for eyebrows (11px, 0.18em, uppercase), header, Visit pill, drag hint, footer.
- Oversized tight-tracked grotesque for the title follows Josephmark; everything else stays quiet.

## Spacing & Layout
- Gutter `clamp(20px, 7vw, 112px)`. Copy column max 520px, always left; the rig sits at +2.1 world units so the object is right of centre.
- Sections `min-height: 100svh`, flex, vertically centred. `main` has `pointer-events: none` so the whole viewport is a drag surface; links, the button and the ramp strip re-enable pointer events.
- Fixed header: exhibition name left, Visit pill right. Drag hint bottom right of the hero. Footer with hairline.
- Breakpoint 760px: object centred in the upper half (rig y +1.9, camera z 23), cells at 78 percent size, copy at the bottom, hint hidden.

## Motion
- Idle rotation 0.3 rad/s; drag with inertia; idle resumes after 2.5 s.
- Section crossfade about 0.5 s (lerp 6 per second) for object weight, cell size and tilt.
- Glyph swaps are instantaneous by design: each frame picks the glyph for the current luminance, so the field flickers at object edges, which is the terminal feel.
- Button hover inverts to white fill, 250 ms. Drag-hint dot slides along its rule every 2.4 s.
- `prefers-reduced-motion`: no spin, no inertia, instant crossfades, static hint.

## Components
- **hero**: eyebrow, h1 fixed on two lines (文字の / 世界, `white-space: nowrap` with a `<br>`), one sentence, drag hint; tsubo at 13 px cells right of centre, turning slowly without input.
- **section**: eyebrow, h2, one sentence; object and cell size per keyframe.
- **ramp strip**: 16 bordered cells, 2em x 2.4em, showing the exact glyphs the shader uses in order.
- **button**: 1px `--ink` border, 999px radius, 14/26px padding, 14px 0.1em, trailing rule.
- **footer**: hairline, `VALE / PLAY 008`, exhibition name.
- **fallback**: `html.nogl` hides the canvas and shows a bordered note under the hero copy.

## Do / Don't
Do
- Keep the picture monochrome; brightness is carried by glyph density first and glyph grey second.
- Thin the field behind text instead of putting a panel under it.
- Pick ramp glyphs by measured ink, and show the ramp to the reader.
- Let the object rotate slowly when idle so the swapping is visible without interaction.

Don't
- No green phosphor, no scanlines, no CRT curvature.
- No colour in the glyph field; the ember stays in the chrome.
- No second typeface for the atlas; the copy face is the picture face.
- No more than one object on screen at a time.

## References
- **Ciridae** (stock/refero/md/ciridae.md): a void canvas with ghost-outlined pills, uppercase mono labels and one ember hairline as the only colour. Abstracted: the void, the hairline-only accent, outlined pill controls. Changed: no blurred photography; the ambient layer is a live glyph field, and type is mixed case Noto Sans instead of condensed uppercase.
- **Josephmark** (stock/refero/md/josephmark.md): midnight gallery, oversized tight-tracked grotesque floating in negative space, flat surfaces and zero shadows. Abstracted: display type at -0.03em against a black stage, chrome that disappears. Changed: the "artwork" on the wall is generated by the typeface itself rather than photographed case studies, and the warm taupe is replaced by a single ember stroke.
