motionsites中文站 / ASCII Studio
ASCII Studio
aadmin2026-10-17 发布Site模板0 阅读

ASCII Studio

类型:Site模板 · 收录:2026-10-17 12:02:21 · 源码:会员可见

[hidecontent type="vip1"]
# Prompt: recreate the "Architecture — ASCII" site (Ublicloud)

Build a one-page marketing site for a fictional architectural 3D-modeling studio called **Ublicloud**. The centrepiece is a real 3D building ensemble rendered live as monospaced ASCII glyphs. Scrolling flies the camera from an isometric overview down into the model, walks it through a covered passage and a circular courtyard to the central tower, then pulls back while the whole canvas shrinks into a square card on a lime "showcase" layout. Below that are regular content sections whose images are also live ASCII views of the same model.

Follow every number below exactly. Where something is not specified, choose the plainest option.

---

## 0. Hard constraints

- Static site, **no framework, no bundler, no build step, no npm dependencies**. Plain HTML, CSS and native ES modules.
- Everything lives in `dist/`. Three.js **r180** is vendored in `dist/vendor/` (`three.module.js`, `three.core.js`, `GLTFLoader.js`, `BufferGeometryUtils.js`, plus the MIT license text) and resolved with an import map: `{ "imports": { "three": "./vendor/three.module.js" } }`.
- `server.mjs` at the root: a dependency-free Node `http` static server for `dist/`, bound to `127.0.0.1`, port `process.env.PORT || 4173`, `/` → `index.html`, path-traversal guard, `Cache-Control: no-cache`, MIME types for html, js, css, glb (`model/gltf-binary`), png, webp, svg, woff2, txt; logs `Local: http://localhost:<port>`.
- `package.json`: `"type": "module"`, scripts `dev` = `node server.mjs`, `check` = `node --check` on every JS file.
- `vercel.json`: `framework: null`, empty `buildCommand` and `installCommand`, `outputDirectory: "dist"`.
- One font only: **JetBrains Mono Medium (weight 500)**, self-hosted at `dist/assets/fonts/JetBrainsMono-Medium.woff2`, `font-display: swap`, preloaded in `<head>`. Every piece of UI text uses `"JetBrains Mono", monospace` at weight 500. (The ASCII glyphs inside the canvas are drawn with `14px "Courier New", monospace`; the hidden debug panel uses the system sans stack.)
- Module files: `preloader.js`, `app.js`, `header.js`, `studio.js`, `model-views.js` (all loaded as `type="module"` in `<head>`), plus `layout.js`, `presentation.js`, `camera-rig.js`, `model-loader.js`, `model-grounding.js`, `architecture-material.js`, `ground-dots.js`, `ascii-effect.js`, `ascii-density.js`, `ascii-settings.js`, `ascii-controls.js`, `ascii-companion.js`, `guide-speech.js`. Styles: `style.css`, `studio.css`, `mobile.css`.

## 1. Assets I will provide (do not generate these)

Place them at these paths and use them as-is:

- `dist/assets/architecture.glb` — the 3D model. **One mesh named "Architecture"**, POSITION + NORMAL only, no materials or textures, about 454k vertices / 937k triangles. Bounds: x −0.47…0.47, y 0…0.334, z −0.49…0.49 (Y up, ground at y≈0). It is an ensemble of six volumes with a tall central tower at the origin. All route and repair coordinates in sections 8–9 are in this model's units and only make sense for this file.
- `dist/assets/logo.svg` — 187×56, a white mark followed by the wordmark "Ublicloud" in `#D2FC52`.
- `dist/assets/menu-complex.png`, `menu-house.png` (1448×1086 each) — realistic isometric architectural model renders on charcoal with fine lime wireframe lines.
- `dist/assets/model-tower.webp`, `model-courtyard.webp`, `model-house.webp`, `model-pavilion.webp`, `model-city.webp` (1448×1086, same realistic style).
- `dist/assets/mono-tower.webp`, `mono-courtyard.webp`, `mono-house.webp`, `mono-pavilion.webp` (4:3, monochrome ASCII-style renders on `#050505`).
- `dist/assets/company-logos/*.svg` — single-colour 32×32 marks: autodesk, blender, nvidia, unity, epicgames, siemens, dassaultsystemes, amd, intel, trimble.

## 2. Design tokens

| Token | Value |
|---|---|
| Page background / near-black | `#050505` |
| Lime accent | `#D2FC52` |
| Ink on lime / light surfaces | `#080a04` |
| Secondary ink on lime | `#3e4436` |
| Body text on dark | `#deded6` |
| Muted text on dark | `#a4a69e` |
| Paper (process section) | `#eef0e8` |
| Menu panel | `#30312e`, text `#f3f3ed`, label grey `#bbbdb4` |
| ASCII glyph colour | `#8f8f8f` |
| ASCII cursor-trail colour | `#d2fc52` |
| Marquee | background `#000`, text `#a3a3a3` |
| Signature easing | `cubic-bezier(0.83, 0, 0.17, 1)` |

No rounded corners anywhere on the site (everything is square) and no shadows. The scrollbar is hidden (`scrollbar-width: none` and `::-webkit-scrollbar { display: none }`), `body { overflow-x: hidden }`, `* { box-sizing: border-box }`. `<meta name="theme-color" content="#050505">`, title `Architecture — ASCII`, description `Ublicloud — detailed 3D models of buildings and architectural complexes, with immersive digital walkthroughs.`

## 3. Page structure (DOM order)

1. `#model-preloader` (fixed full-screen)
2. `header.site-header` (fixed) — logo, Menu button, CTA button. Starts with `inert` and `data-preloader-lock`.
3. `<main inert data-preloader-lock>`
   - `.journey` (height **920svh**) containing one sticky `section.hero` (100svh) with: `.showcase-background`, `.showcase-layout.composition`, `#composition.composition` (holds the `h1.hero-heading`), `.model-frame > #model-stage.model-stage > canvas#architecture`.
   - `.continuation` with five sections: `#how-we-work` (process), `#selected-concepts` (work), `#model-detail` (standards), `#questions`, `#start-project`.
4. `.logo-marquee` (fixed bottom)
5. `dialog#studio-menu`
6. Hidden ASCII control panel (`#ascii-controls-open` button and `aside#ascii-controls`)

`<html class="model-loading">` at load; `html.model-loading { overflow: hidden }`.

## 4. Preloader

- Fixed, inset 0, z-index 100, background `#050505`, colour `#d2fc52`, grid-centred, `touch-action: none`.
- Counter: `font: 500 clamp(96px, 22vw, 360px)/1`, letter-spacing −0.07em, flex baseline, gap 0.08em. Number span has `min-width: 3ch`, right-aligned, tabular numerals. A `%` span at 0.28em, letter-spacing −0.04em. `role="progressbar"`, `aria-valuenow` updated.
- Bottom bar: absolutely positioned, left/right/bottom 48px (20px and `max(24px, safe-area)` at ≤900px), 4px tall, track `#d2fc5229`, fill `#d2fc52` with `transform: scaleX(var(--load-progress))`, origin left.
- Progress logic: target = download fraction × 84 while the GLB downloads, 90 when parsed, then 92 → 95 → 97 as the scene is prepared; targets only ever rise and are capped at 97 until completion. Completion (target 100) fires after the **first ASCII frame has rendered and `document.fonts.ready` resolved**. Displayed value eases toward the target each frame with `displayed += (target − displayed) × (1 − exp(−dt / 180ms))`; integers shown are `floor(displayed)`. Minimum on-screen time 1400ms.
- Exit: state `complete` → wait 180ms → state `leaving` (opacity 0, `transition: opacity 550ms ease`) → after 550ms hide, remove `model-loading` from `<html>`, remove `inert` from every `[data-preloader-lock]`.
- Failure (script error, unhandled rejection, model load error): stop the counter and reveal `.preloader-error` at top 68%: text "The 3D model could not load." (14px/1.5) and a lime button "Try again" (1px lime border, padding 12px 20px, black text) that reloads the page.
- With reduced motion: no easing, no minimum time, no fade.

## 5. Header (fixed, z-index 8)

Positioned top 48px, left 48px, right 48px; grid `minmax(0,1fr) auto`; `pointer-events: none` on the bar, `auto` on the controls. All three controls are 56px tall.

**Logo** — left, 187px wide.

**Menu button** — absolutely centred (`left: 50%; translateX(-50%)`), 320px wide (220px at ≤1000px), 56px tall, padding 0 16px, flex space-between: the word "Menu" (14px/1.4, white) and a two-line icon (32×24 box, two 2px-thick 32px lines at top 7px and 17px). Background `rgba(255,255,255,0.08)` with `backdrop-filter: blur(40px)`; hover `rgba(255,255,255,0.14)`; focus ring 2px lime, offset 5px.

**CTA button "LET'S WORK TOGETHER"** — right column. Uppercase 14px/1.4, text `#050505`. It is built from three lime (`#D2FC52`) blocks, each 56px tall, with a 6px gap:
- a 56×56 square with a plus icon on the left (plus is a 12×12 viewBox SVG, 0.75em, stroke 1.5, square caps),
- a label block with padding 0 16px,
- a 56×56 plus square absolutely positioned at the right edge.

Resting state: left square is `rotate(-45deg) scale(0)` (origin left centre), the label is shifted left by 62px (`translateX(-(56px + 6px))`) so it sits flush left, and the right square is visible. On hover/focus: left square grows to `rotate(0) scale(1)`, label slides to `translateX(0)`, right square collapses to `rotate(-45deg) scale(0)` (origin right centre). All three transition `transform 700ms cubic-bezier(0.83,0,0.17,1)`. Hovering also runs the **one-shot text scramble** (section 15) on the label.

**Light-surface switching.** Every scroll frame, for each of the three header items, decide whether the page behind its centre point is light:
- if its vertical centre is inside a `.continuation section[data-surface]` or the mobile details block, use that section's surface (`data-desktop-surface` wins over `data-surface`);
- otherwise (still in the hero) it is "light" when its centre lies **outside the current rectangle of the shrinking model frame** (because the lime showcase background is showing there).

When light, add `.is-on-light`: logo gets `filter: brightness(0)`; the Menu button becomes text `#050505` on `#0505050d`; the CTA inverts to lime text on `#050505` blocks. Transitions 250ms ease.

## 6. Hero composition and heading

**Artboard.** All hero overlay content is laid out on a fixed **1600×1000** artboard that is uniformly scaled: `scale = min(heroWidth/1600, heroHeight/1000)`, written to `--composition-scale` by a `ResizeObserver`. `.composition` is `position: absolute; width: 1600px; height: 1000px; left: 50%; bottom: 0; transform: translateX(-50%) scale(var(--composition-scale)); transform-origin: center bottom; pointer-events: none; z-index: 2`.

`#composition` (the heading layer) instead uses `top: var(--hero-composition-top); bottom: auto; transform-origin: center top`. On desktop, `--hero-composition-top = 48 + 56 + 64 − modelTop × scale` px, where `modelTop` is the artboard Y of the top of the model's silhouette in the overview. This pins the top of the model exactly 64px below the Menu button; any spare viewport height falls below the composition.

**Heading.** `h1.hero-heading`: absolute, `top: 490px; left: 50%; transform: translate(-50%, -50%); width: 1400px; text-align: center; font: 500 25.6px/1.45; color: #050505`. Five rows, each a block containing an inline-block span with background `#D2FC52` and padding `0 7px` (so each line reads as a lime highlighter strip sitting over the dark ASCII model):

1. Your vision, in 3D.
2. Buildings shaped into detailed models.
3. Explore your future space from every perspective.
4. From the skyline down to street level.
5. Before it is built.

Its opacity is `clamp(1 − screens / 0.48)` where `screens = scrollY / heroHeight`, so it is gone after about half a screen of scroll. Hovering it runs the **continuous scramble** (section 15).

**Canvas layers.** `.model-frame`: absolute, top-left, 100%×100%, `overflow: hidden`, background `#050505`, z-index 1. `.model-stage` inside it: same size, `transform-origin: top left`. The canvas fills the stage, starts at opacity 0 and fades in over 800ms when `.hero[data-ready="true"]`. Cursor: `pointer` when hovering a building in overview, `grab` in street mode, `grabbing` while dragging.

## 7. ASCII rendering pipeline (Three.js)

Two passes on one `WebGLRenderer` (`antialias: false`, `alpha: false`, `powerPreference: 'high-performance'`, pixel ratio 1, sRGB output, clear `#050505`):

1. Render the 3D scene into a `WebGLRenderTarget` (nearest filtering, depth buffer, `NoColorSpace`) cleared to transparent. Materials do not output colour; they output **data channels**.
2. Render a full-screen quad whose fragment shader turns that data into glyphs.

Raster size for both = viewport × `min(devicePixelRatio, 1.5)` (1.25 on mobile). The post shader works in **logical design pixels**: `uResolution = (heroWidth / scale, heroHeight / scale)`, so glyph size scales with the artboard.

### 7.1 Data channels

- **R** = live luminance (with moving light), **G** = building-region id / 8 (1.0 marks the guide cube), **B** = stable luminance × 0.24 for architecture, **0.75 for ground**, **A** = 1 where anything was drawn.
- Classification in the post pass: `a > 0.5 && b < 0.25` → architecture glyph cell; `a > 0.5 && b > 0.25` → ground dots; otherwise empty background.

### 7.2 Architecture surface shader

World-space normal `n`. Constants: fixed key direction `normalize(−0.80, 0.50, 0.36)`, fixed fill `normalize(0.73, 0.60, −0.14)`, ribbon axis `normalize(0.85, 0.10, 0.45)`.

```
key   = max(dot(n, fixedKey), 0)
fill  = max(dot(n, fixedFill), 0)
contact = 0.020 * exp(-max(worldY - baseY, 0) / 0.012) * (1 - |n.y|)     // baseY 0.0035, or 0.021 for the tower region
stable = clamp(0.24 + key*0.43 + fill*0.13 + max(n.y,0)*0.14 - contact, 0.22, 0.78)

// moving accent, driven by time t (seconds):
azimuth   = -1.0 + sin(t*0.24)*0.70
elevation = 0.61 + sin(t*0.13)*0.07
movingKeyDir = (sin(az)cos(el), sin(el), cos(az)cos(el))
reflectionDir = normalize(sin(t*0.19 - 1.1), -0.45, cos(t*0.19 - 1.1))
ribbonOffset  = sin(t*0.28) * 0.40

specular = pow(max(dot(n, normalize(viewDir + reflectionDir)), 0), 24)
ribbon   = exp(-(dot(worldPos, ribbonAxis) - ribbonOffset)^2 / 0.0036)
movingDiffuse = clamp(dot(n, movingKeyDir) - dot(n, fixedKey), -1, 1) * 0.040
movingShade   = -0.022 * (1 - max(dot(n, movingKeyDir), 0)) * ribbon
change = clamp(movingDiffuse + specular*ribbon*0.10 + movingShade, -0.06, 0.10)
headroom = change >= 0 ? 0.84 - stable : stable - 0.20
limit    = change >= 0 ? 0.10 : 0.06
luminance = stable + change * headroom / (headroom + limit)
out = (luminance, region, stable * 0.24, 1)
```

The effect: volumes keep a fixed studio light, while a soft band of light slowly sweeps across the facades.

### 7.3 Ground shader

An 8×8 plane at y = 0.0035 (double-sided) plus the walkable floor strips:

```
street = 1 - smoothstep(0.16, 0.85, distanceXZ(worldPos, cameraPos))
fade   = mix(1, street, streetBlend)          // streetBlend = smoothstep(progress*(1-reveal), 0.12, 0.42)
glow   = 0.90 + 0.07*sin(dot(xz, (16,12)) - t*0.75) + 0.03*sin(dot(xz, (-41,27)) + t*1.10)
out = (fade*glow, 0, 0.75, 1)
```

### 7.4 Glyph atlas

Draw the symbol string into a 2D canvas, one **12×16 px tile** per symbol, white on black, `14px "Courier New", monospace`, centred, linear filtering, no mipmaps. Default alphabet, light to dense: `.:-=+*#%@0ONMWBX` (16 glyphs). A second one-tile atlas contains only `.` for the ground.

### 7.5 Post-pass fragment shader

Uniform values in use: glyph colour `#8f8f8f`, trail colour `#d2fc52`, guide colour `#D2FC52`, background `#050505`, contrast **2.15**, brightness 0, empty threshold 0.12, shimmer 0.012, glyph interval 0.35–0.55 s, glyph speed **2.35**, trail radius 26, trail lifetime 2.2 s, trail selection 0.78.

```
pixel = vUv * uResolution
grid  = (pixel - uResolution/2) / uCell          // grid is centred on the screen
cellIndex = floor(grid)
cellHash  = fract(sin(dot(cellIndex, (127.1, 311.7))) * 43758.5453)
cellUv    = ((cellIndex + 0.5) * uCell + uResolution/2) / uResolution
s = texture(scene, cellUv)                        // one sample per cell
```

**Ground:** sample the scene at the true pixel (not per cell). If it is ground, tile the `.` atlas on its own fixed grid `uDotSpacing = (5.2, 7.8)` and set the background to `mix(bg, glyphColour * 0.20812 * R, dotMask)`.

**Architecture cell:**
- `stableLight = clamp(B / 0.24)`, `liveAccent = R − stableLight`
- `light = clamp((stableLight + liveAccent*0.75 − 0.5) * contrast + 0.5)`; `surfaceLight` is the same with `0.35` instead of `0.75`.
- Sample the four neighbouring cells. `silhouette` = 1 if any neighbour is empty or ground. `toneEdge` = largest |Δ stable light| to an architecture neighbour; `edge = max(silhouette, smoothstep(0.14, 0.30, toneEdge))`. `innerFold = smoothstep(0.08, 0.26, brightest positive Δ)`, `foldRim = smoothstep(0.08, 0.26, largest negative Δ)`.
- `glyphDensity = clamp((light − 0.5) * 1.15 + 0.5)`, plus shimmer: `(sin(dot(cellIndex,(0.073,0.051)) − t*0.55) + 0.45*sin(dot(cellIndex,(−0.037,0.089)) + t*0.31)) * 0.012`; then `max(glyphDensity, edge * 0.28)`.
- Draw a glyph only if `light >= 0.12 || edge > 0.5`.
- `tile = clamp(floor(glyphDensity * count), 0, count−1)`.
- **Glyph flicker:** `tempoHash = fract(sin(dot(cellIndex,(269.5,183.3))) * 43758.5453)`, `interval = mix(0.35, 0.55, tempoHash)`, `phase = mod(floor(t * 2.35 / interval + cellHash * 4), 4)`, offset = −1, 0, +1, 0 for phases 0–3; add to `tile`, reflecting at both ends (never clamp). Every cell therefore keeps swapping between neighbouring-density characters on its own tempo.
- Sample the atlas at `((tile + 0.02 + fract(grid).x*0.96) / count, 0.02 + fract(grid).y*0.96)`.

**Cursor trail:** up to 64 points `(x, y, time, strength)` in design pixels. For an architecture cell, for each live point within radius 26: `probability = 0.78 * (1 − smoothstep(0, 26, distance))`; if `cellHash < probability`, `trail = max(trail, pow(1 − age/2.2, 1.35))`. Whole cells switch colour; there is no glow or blur.

**Final colour:**
```
glyphColour = mix(#8f8f8f, #d2fc52, trail);  if (G > 0.95) glyphColour = #D2FC52   // guide cube
ink = (0.10 + 0.90 * pow(surfaceLight, 1.05)) * (1 - innerFold*0.15) + foldRim*0.07
glyphColour *= clamp(max(ink, silhouette*0.66, isGuide ? 0.82 : 0))
out = mix(background, glyphColour, glyphMask)       // then sRGB conversion
```

### 7.6 Cell size

Base cell is 5.2×7.8 design px, close-up cell 7.0 wide; height is always 1.5× width. Per frame:

```
walkingCell = lerp(5.2, 7.0, smoothstep(progress*(1-reveal), 0.06, 0.38))
magnification = pixelsPerWorldUnit(activeCamera at visible depth) / pixelsPerWorldUnit(overview)
magCell = 5.2 + 1.8 * smoothstep01(log2(max(1, magnification)) / 4)
desired = max(walkingCell, magCell) * (6.8 / 5.2) * (mobile ? min(1, 450/heroWidth) : 1)
currentCell = lerp(currentCell, desired, 1 - exp(-dt*7))
```

So glyphs are 6.8 design px wide in the overview and grow to about 9.2 while walking. Visible depth comes from a ray through the screen centre against invisible proxy boxes for the six buildings (centres/half-sizes/heights: (−0.142,−0.307)/(0.163,0.184)/0.125; (0,0)/(0.08,0.08)/0.334; (0.195,0.284)/(0.122,0.192)/0.13; (0.4,−0.02)/(0.07,0.08)/0.22; (−0.37,0.03)/(0.1,0.16)/0.08; (−0.118,0.24)/(0.105,0.13)/0.055). The same proxies drive the hover cursor.

### 7.7 Frame loop

`requestAnimationFrame`, `dt` capped at 0.05s, smoothing factor `1 − exp(−dt × 7)` for scroll progress, reveal, parallax and look offsets. When nothing is being interacted with, cap rendering to 30fps (the light and glyph flicker keep running). Pause entirely when the hero is off-screen (IntersectionObserver) or the tab is hidden. On `webglcontextlost` hide the canvas; on restore, reload.

## 8. Runtime model repair ("grounding")

The GLB's passage is blocked and its court has no floor, so after loading, edit the runtime copy only:

- Replace every mesh material with the architecture shader; disable frustum culling.
- **Delete triangles** that obstruct a corridor following the walking route (clear half-width 0.009, from 0.009 below eye height to 0.006 above it), and delete the damaged low assemblies at the hall (x −0.0615…−0.0215, z 0.299…0.492, below y 0.040), around the two gates (z 0.309→0.245 "Court entry" and z 0.178→0.131 "Court exit", repair half-width 0.019), the court-entry corner (x −0.1165…−0.0900, z 0.2340…0.2555), a thin inner-wall arc of the court (centre (−0.118, 0.192), radii 0.050–0.068, angles −65°…28°), and the tower's low portal frame (x −0.018…0.038, z 0.029…0.061, y 0.0205…0.080).
- **Add closed solids** in their place: a tower foundation box 0.160×0.0175×0.142 centred (0.002, ·, −0.006) from y 0.0035 to 0.021; hall floor, a canopy at y 0.026–0.036, two rows of five 0.004×0.005 columns at x = −0.056 and −0.028 (z = 0.308, 0.3295, 0.351, 0.3725, 0.394), low entry edges; for each gate a floor, left and right walls (offsets 0.0115–0.016 from the route, y 0.007–0.027), a canopy up to 0.037 and four jambs; a capped circular court wall (radii 0.054–0.069, top 0.0357); tower entrance jambs at x = −0.009 and 0.029 (0.008×0.059×0.016, z 0.045) and a lintel 0.046×0.007×0.016 at y 0.0765.
- Add a walk surface: a ribbon 0.016 wide along the route, top at (route y − 0.009), using the ground shader.

## 9. Camera

Shared near 0.0005, far 10.

**Route** (camera eye positions, centripetal Catmull-Rom, not closed):
```
(-0.042,0.016,0.481) (-0.042,0.016,0.440) (-0.042,0.016,0.395) (-0.042,0.016,0.345)
(-0.042,0.016,0.295) (-0.058,0.016,0.263) (-0.085,0.016,0.234) (-0.090,0.016,0.202)
(-0.070,0.016,0.172) (-0.042,0.016,0.145) (-0.013,0.016,0.112) (0.004,0.018,0.095) (0.008,0.024,0.085)
```
Entrance aim point: `(0.010, 0.070, −0.025)`.

**Overview (progress 0).** Orthographic, isometric: yaw −45°, elevation 30°, 2.5 units from the focus. Fit: project every vertex into camera space, recentre the focus on the projected bounding box, `halfWidth = max(projHeight/2 × (1600/730), projWidth/2) × 1.04 / 1.4`. World units per design pixel = `2 × halfWidth / 1600`. The frustum covers the whole logical viewport at that scale and is shifted vertically so the model's centre sits at artboard Y **620** (plus the composition top offset).

**Journey (progress `p` = clamp(scrollScreens / 6)).** `FLIGHT_END = 0.42`. `ease` = smootherstep.
- `intro = clamp(p / 0.42)`. `framing = 1 − ease(intro)` scales the off-centre vertical shift down to zero.
- **Flight (0 < p < 0.42):** perspective camera moves along a cubic Bézier: P0 = overview position, P1 = focus + (−0.38, 0.88, 0.88), P2 = route start + (0, 0.16, 0.14), P3 = route start, at parameter `ease(intro)`. It looks at `lerp(focus, landingTarget, ease((intro − 0.5)/0.5))`.
- **Walk (p ≥ 0.42):** position = route at `ease((p − 0.42)/0.58)`, looking 0.08 ahead along the tangent. Over the last 13% of the route the direction blends toward the entrance aim point (smoothstep 0.87→1), so the camera tilts up at the tower.
- **Projection morph:** street FOV 60°. The perspective FOV starts at the value that matches the orthographic framing at distance 2.5 and eases to 60° over `intro` 0.65→1. For `p` in 0→0.15, interpolate **all 16 projection-matrix elements** from the orthographic matrix to `perspective / 2.5` with `smoothstep(p, 0, 0.15)` so the ortho-to-perspective change is seamless.

**Reveal (pull-back).** `reveal = ease((screens − 6) / 1.2)`. The camera lerps position from the end of the walk to the overview position, its look target from "0.08 ahead" to the focus, and FOV to the fitted value; over reveal 0.75→1 the projection matrix blends back to orthographic. Simultaneously the overview's scale tightens so the model fits the square card: units = `lerp(base, 2 × halfWidth × 1.4 / (min(viewW, viewH) × 1.18), reveal)`, vertically centred.

## 10. Scroll choreography

`screens = scrollY / heroHeight`. Total journey is 8.2 screens (920svh − 100svh).

| Screens | What happens |
|---|---|
| 0 | Isometric ASCII overview, lime heading over it, marquee at the bottom |
| 0 → 0.48 | Heading fades out |
| 0 → 2.52 | Flight: camera swoops down to the passage entrance; ortho → perspective |
| 2.52 → 6 | Walk through the hall, court entry gate, circular courtyard, exit gate, up to the tower door |
| 6 → 7.2 | Reveal: camera pulls back to the overview while the canvas shrinks into the showcase card |
| 7.2 → 8.2 | Showcase holds, then the page scrolls on to the content sections |

**Frame shrink.** Each frame: `width = mix(heroWidth, cardWidth, reveal)`, same for height, `x/y = mix(0, cardLeft/Top, reveal)` where the card is `.showcase-model-anchor`. Apply to `.model-frame` as width/height + `translate3d`. The stage inside keeps the hero's full size and is scaled by `max(width/heroWidth, height/heroHeight)`, centred — a cover crop. Behind it the lime showcase is revealed.

**Showcase copy** fades and rises in at the very end: `copy = ease((reveal − 0.78) / 0.22)`; elements with `.showcase-copy` use `opacity: copy; transform: translateY((1 − copy) × 24px)`.

**Marquee** opacity = `clamp(1 − reveal × 2)`.

**Guide-cube and speech** appear during the walk (section 11).

## 11. Guide cube and speech bubbles

A tiny cube (edge 0.0037 world units) leads the camera along the route during the walk.

- `walking = smootherstep(p, 0.42, 1)`, `lead = min(0.055, (1 − walking) × 0.65)`, position = route at `min(1, walking + lead)`, raised by 0.001.
- `presence = smootherstep(p, 0.36, 0.46) × (1 − smootherstep(p, 0.96, 1)) × (1 − smootherstep(reveal, 0, 0.20))`; scale = `presence × min(1, lead / 0.055)`; hidden below 0.001.
- Rotation `(t × 0.31, t × 0.63 + walking × 2π, t × 0.17)`.
- Its shader writes `G = 1`, so the post pass draws its glyphs in **lime**. Flat-shaded light `0.36 + 0.40 × key + 0.12 × fill`, with cube edges raised to 0.94.

**Speech bubble** (`aside.guide-speech`, appended to the hero, z-index 3): lime background, `#050505` text, `font: 500 14px/1.5`, padding 11px 15px, centred text, `max-width: min(320px, 100% − 40px)`, with a 10×10 lime square rotated 45° poking out of the top centre as a pointer. It is positioned 12px below the cube's projected screen point, clamped 20px from the sides and between 110px from the top and 90px from the bottom.

Phrases by journey progress:
- from 0.48: "Follow me. Let's step inside."
- from 0.68: "The courtyard brings the spaces together."
- from 0.86: "Look up. This is the central tower."

Enter: 480ms, opacity 0→1 with `translate: 0 8px → 0 0`, easing `cubic-bezier(.22, 1, .36, 1)`. Changing phrase: only after the current one has been visible ≥3200ms; fade out 340ms ease-in-out with `translate 0 −6px`, 180ms gap, then enter. Hide when the cube is invisible, off-screen, or the reveal has started. `role="status"`, `aria-live="polite"`.

## 12. Pointer and keyboard interaction on the canvas

- **Parallax (overview):** moving the mouse rotates the overview by up to ±0.028 rad yaw and ±0.012 rad tilt (pointer position in −1…1), smoothed; fades out over the first 60% of the flight; resets on pointer leave.
- **Cursor trail:** on mouse move, at most one trail point every 45ms; nearby glyph cells turn lime and fade over 2.2s. Cleared on resize and whenever scroll progress changes.
- **Look around (street mode, p ≥ 0.42, before reveal):** drag to look — yaw `−dx × 0.004`, pitch `−dy × 0.003` clamped to ±1.1 rad. On touch, start only after 8px of mostly-horizontal movement (`touch-action: pan-y` keeps vertical scroll). Arrow Left/Right turn ±0.10 rad. Look offset resets when progress drops below 0.01.

## 13. Showcase layout (on the 1600×1000 artboard)

Background: solid `#d2fc52` with a drifting dot texture on top — `radial-gradient(circle, #fff 1.2px, transparent 1.5px)` at 12×12px, opacity 0.28, animating `background-position` to `12px 12px` over 18s linear infinite.

Text colour `#080a04`, base `500 14px/1.6`.

- **Card anchor:** left 580, top 280, 440×440 (the ASCII canvas shrinks into this).
- **Intro:** left 100, top 360, width 350. Eyebrow "Concept ensemble / 3D study" (11px, uppercase, letter-spacing 0.04em, margin-bottom 16px). `h2` 32px/1.25, letter-spacing −0.04em: "Architecture," / "from every angle."
- **Description:** left 100, bottom 280, width 350: "A central tower, curved volumes and stepped facades form one connected place. Explore its silhouette, then follow the covered passage through the courtyard to discover the architecture at street level."
- **Facts:** left 1110, top 500, width 370, vertically centred on that point. Three items separated by 1px `#080a0424` rules (padding 18px 0 20px), each an `h3` (22px/1.2, −0.04em) and a 13px/1.5 paragraph:
  - **A central anchor** — The tallest tower gives the ensemble a clear focal point and completes the walking route.
  - **Spaces that connect** — A covered passage links the entrance, circular courtyard and main building.
  - **Skyline to street** — The same model reveals its overall composition from above and its human scale from within.
- **Caption:** left 580, top 736, width 440, 9px uppercase, space-between: "Live 3D / ASCII" and "Scroll up to retrace the route".

Headings here run the continuous scramble on hover.

## 14. Content sections (`.continuation`)

`--content-scale = min(innerWidth/1600, innerHeight/1000)`, updated on resize (keep the reader's place by restoring the same fraction of the current section). `.section-wrap`: width `1400px × scale`, max `100% − 96px`, centred. On desktop: section titles `32px × scale`/1.25, −0.04em; `h3` and step titles `22px × scale`; body `max(12px, 14px × scale)`/1.6.

### 14.1 "How we work." — `#how-we-work`

Background `#eef0e8`, text `#080a04`. Section height **400svh** with a sticky 100svh pin, content vertically centred. Grid of two equal columns, column gap `120px × scale`, row gap `56px × scale`; the title spans both columns.

Left: four step buttons, gap `26px × scale`. Each has a square lime marker (`12px × scale`) and a title + copy. Inactive: colour `#61655a`, opacity 0.55. Active: `#080a04`, opacity 1, `translateX(14px × scale)`, marker rotated 12°. Transitions 450ms ease.

1. **Start with your idea.** Plans, sketches or a simple brief. We define the building, the level of detail and how you want to explore it.
2. **Shape the architecture.** Volumes, proportions and circulation come first. See how the building sits in its surroundings before we refine it.
3. **Build in the detail.** Facades, materials and structural elements turn the geometry into a clear, considered architectural model.
4. **Step inside.** Review the final views and follow a route through the space. From the whole composition to the details at street level.

Active step = `floor(pinProgress × 4)`. Clicking a step smooth-scrolls to `sectionTop + travel × ((index + 0.2) / 4)`.

Right: a 4:3 panel on `#050505` showing a **live ASCII view** of the model (section 14.5); the view index follows the active step. The static `mono-*.webp` images are stacked underneath as a fallback until the live canvas is ready (crossfade 650ms opacity, scale 1.04 → 1 over 1.2s).

### 14.2 "One model. Every scale." — `#selected-concepts`

Background `#d2fc52`, text `#080a04`, padding `170px × scale` top and bottom. Grid `400fr 880fr`, gap `120px × scale`.

Left (sticky at `max(140px, 170px × scale)`): title "One model." / "Every scale."; description (colour `#3e4436`, max-width `400px × scale`): "Explore the same architecture from its full composition down to the smallest structural detail."; then a nav of four rows, each a `100px × scale` wide 4:3 live ASCII thumbnail plus a label (`max(12px, 13px × scale)`, colour `#3e4436`, active/hover `#080a04`): **Ensemble**, **Circular volume**, **Facade / macro**, **Structure / micro**. The active row is the project whose top is nearest 25% of the viewport height; clicking smooth-scrolls to it with an offset of `max(140, 160 × scale)` px.

Right: four articles, gap `100px × scale`, each a 4:3 live ASCII panel on `#050505` followed by an `h3` and a paragraph (`#3e4436`):

1. **The whole ensemble** — The complete composition: towers, curved volumes and connected passages.
2. **The circular volume** — A closer angle reveals the rounded facade and the depth of its levels.
3. **Facade / macro** — A close view of the central tower, where light brings out the facade relief.
4. **Structure / micro** — From the whole building to a single column and the structure above it.

### 14.3 "Common questions." — `#questions`

Background `#d2fc52`, text `#080a04`, padding `170px × scale`. Grid `480fr 800fr`, gap `120px × scale`. Left: title "Common" / "questions." Right: six native `<details>` rows with a 1px `#080a0426` bottom border; summary is flex space-between at `22px × scale`, padding `28px × scale` 0, with a `+` (`24px × scale`) that rotates 45° when open (300ms). Only one may be open at a time. Answers (`#3e4436`, body size, padding-bottom `30px × scale`, padding-right 32px) animate in: 350ms, opacity 0 → 1 and `translateY(−8px)` → 0.

- **What do you need to start?** Share the plans, sketches or references you have, along with the building’s purpose. We can then define what needs to be modeled and which details matter most.
- **Can we begin with a concept?** Yes. A concept model focuses on volume, proportion and the relationship between spaces. It can become more detailed as the design develops.
- **How detailed should the model be?** That depends on what you need to see. An early study may need clear massing; a presentation may call for facade elements, materials and an interior route. We set that scope together.
- **Can I explore it at street level?** A walkthrough can follow a defined route through the building and its surroundings. The model on this page demonstrates the transition from the skyline to the central courtyard.
- **How are changes reviewed?** We compare the model with the agreed brief and your feedback at each stage. Proportions come first, followed by the details and the final presentation.
- **What will the final delivery include?** The model, views and presentation are agreed before work begins. File formats, detail levels and any walkthrough are chosen for the way you plan to use them.

This is the last visible section; the page ends here.

### 14.4 Two sections present in the markup but hidden at every width

Keep them in the HTML with `display: none` applied by the current stylesheets:

- `#model-detail` "More than / a silhouette." — a 3-column grid of six cards: *Proportions that hold.* / *Facades with depth.* / *Structure you can read.* / *Materials in context.* / *A human perspective.* / *A model with purpose.*, each with one sentence of copy.
- `#start-project` "Start a project." — `model-city.webp`, heading "Your next building. / Before it’s built.", the line "Bring your plans, your references or an idea you want to see in three dimensions.", and a black "Prepare your brief +" disclosure listing: The building and its purpose · Plans, dimensions and references · The details you want to explore · Your preferred views and timeline.

### 14.5 Live ASCII panels (`model-views.js`)

Created lazily the first time the process or work section comes within 300px of the viewport. Uses a **second, off-DOM WebGLRenderer**, one scene (same GLB download shared through a cached loader promise, same grounding, same shaders), and four cameras on a 1600×1200 design frame:

| # | Name | Camera |
|---|---|---|
| 0 | The whole ensemble | Orthographic from focus + (−1.5, 1.1, 1.5), fitted with 8% margin |
| 1 | The circular volume | Perspective FOV 43°, position (−0.60, 0.37, 0), aim (−0.142, 0.06, −0.307) |
| 2 | The central tower facade | FOV 34°, position (−0.19, 0.255, 0.165), aim (0, 0.205, 0.005) |
| 3 | An entrance column and its lintel | FOV 40°, position (−0.042, 0.058, 0.098), aim (−0.009, 0.065, 0.045) |

For each visible panel: render scene → target → ASCII pass on the shared renderer, then `drawImage` the result onto that panel's own 2D `<canvas>`. Panel raster width ≤1000px (thumbnails ≤180px) at `min(dpr, 1.25)`. ASCII settings: cell 6.8×10.2, the same contrast/colours/speed/trail as the hero, logical resolution 1600×1200. Loop at 24fps (20 on mobile); thumbnails repaint once per second. Only visible panels are painted (IntersectionObserver); pause on hidden tab. Mouse movement over a full panel leaves the same lime glyph trail. Each panel gets `role="img"` and `aria-label="Live ASCII 3D model: <name>"`.

## 15. Text scramble effect

Glyph pool: `!<>-_/?%+=@#$0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ`. Updates every 40ms. Spaces are never replaced. Triggered by mouse (not touch) and keyboard focus.

- **One-shot** (CTA label): on enter, every not-yet-revealed character is random; characters resolve left to right over 550ms; done at 650ms.
- **Continuous** (hero heading, showcase headings, every `h2`/`h3` in the content sections): while hovered, all characters keep re-randomising. On leave, "settle" over 700ms: characters lock in left to right following `1 − (1 − t)²`, and unrevealed ones freeze at their last random glyph. Multi-line headings stagger each line by 80ms.

Headings carry an `aria-label` with the real text and their animated spans are `aria-hidden`. All headings are `width: fit-content` so only the text itself is the hover target.

## 16. Menu (`<dialog>`, opened with `showModal()`)

Full-viewport transparent dialog, backdrop `#05050520`. The visible panel `.menu-shell` is pinned to the top, **24svh tall**, background `#30312e`. Closed: `translateY(−110%)`, opacity 0. Open: `translateY(0)`, opacity 1; `transition: transform 750ms cubic-bezier(0.83,0,0.17,1), opacity 450ms ease`. Closing reverses, then `dialog.close()` after 750ms. Lock page scroll while open; trap Tab focus; Escape and backdrop click close; focus returns to the Menu button.

- **Close button:** top 20px, right 48px, 112×36, 12px text "Close", with a 20px ✕ made of the two icon lines rotated ±45°.
- **Body:** inset `20px 184px 20px 48px`, three columns `1.35fr 0.9fr 1.1fr`, gap 32px.
  - **Navigation** (two-column grid, gap 4px 16px, vertically centred): Home, 3D Models, Services, About Studio, Walkthrough. Font `clamp(13px, 1.1vw, 20px)`/1.25. Each item sits in an `overflow: hidden` row and rises from `translateY(110%)` over 650ms with the signature easing, staggered 0/60/100/140/180ms. Hover/active: lime text, `padding-left: 0.75em`, and a 0.38em lime square scales in on the left.
  - **Information:** small uppercase grey label (10px, letter-spacing 0.06em) "Ublicloud / Studio" and "Explore architecture in detail. From the first concept to a digital walkthrough." Clicking **Services** swaps it for label "Services" and "Building models, facade details and immersive digital walkthroughs." Clicking **About Studio** swaps back.
  - **Cards:** two image cards (`menu-complex.png` "Digital architecture", `menu-house.png` "Form and structure"), image `object-fit: cover` on `#10140f`, 9px uppercase caption, image scales to 1.04 on hover over 700ms.
- **Navigation targets:** after the menu has closed, smooth-scroll to `journeyProgress × 6 × heroHeight`: Home = 0, 3D Models = 0.08, Walkthrough = 0.43.

## 17. Logo marquee

Fixed to the bottom, z-index 7, 80px tall, background `#000`. Items: 30px logo (`filter: invert(1); opacity: 0.65`) + name in `500 15px/1.2`, colour `#a3a3a3`, gap 12px; groups have 80px gaps and 40px side padding, `min-width: 100vw`. Order: Autodesk, Blender, NVIDIA, Unity, Epic Games, Siemens, Dassault Systèmes, AMD, Intel, Trimble. Duplicate the group once in JS (`aria-hidden`) and animate the track `translateX(0 → −50%)` linearly, with duration = `groupWidth / 24` seconds (a constant **24px per second**). Pause on hover. It fades out during the reveal (section 10).

## 18. Hidden ASCII control panel

Include a debug panel, disabled by a constant `ASCII_CONTROLS_ENABLED = false` (both the panel and its opener stay `hidden`). When enabled it is a 320px dark rounded card at top-right (system sans font, orange `#ff986f` accents) with: Cell size (3–12, step 0.1), Character set select (Architecture `.:-=+*#%@0ONMWBX`, Minimal `.:-=+*#@`, Classic `.,:;i1tfLCG08@`, Binary `01`, Custom — 1–32 printable ASCII characters), Contrast (0.4–2.4), two colour pickers (Characters, Trail), Glyph speed (0.25–3), Trail density (0–80%), Trail fade (0.3–2.8s), a Reset button, and persistence in `localStorage` under `architecture-ascii-controls-v1`. The site's defaults are: cell 6.8, alphabet Architecture, contrast 2.15, characters `#8f8f8f`, trail `#d2fc52`, speed 2.35, density 78, fade 2.2.

## 19. Mobile (≤900px)

- Header: top `max(16px, safe-area)`, sides 20px, flex space-between; logo 140×42; CTA hidden; Menu button 116×44, 13px, 24px icon.
- Hero heading: not on the scaled artboard; `top: clamp(88px, 18svh, 152px); left: 20px; width: calc(100% − 40px)`, `font-size: clamp(21px, 5.8vw, 30px)`, line-height 1.5, row padding 3px 6px. Shorter copy: "Your vision." / "Buildings, in 3D." / "Explore every detail." / "Before it's built." / "Your space."
- Canvas: logical width is fixed at 900 units (`renderScale = width / 900`); cell size additionally multiplied by `min(1, 450 / heroWidth)`; ground dot spacing 8 CSS px. Overview is centred at 62% of the height (58% and shifted right in landscape).
- Showcase: title at left 20, top 110, 28px; card anchor centred at top 216, width `min(100vw − 40px, (100svh − 250px) × 4/3, 640px)`, 4:3; caption below it at 11px; eyebrow hidden. The description and facts move out of the hero into a separate lime block (`.mobile-showcase-details`, padding 24px 20px 64px, 15px/1.6) placed directly after the journey, pulled up with a negative margin so it follows the card.
- Menu: panel `min(560px, 88svh)` tall, backdrop `#05050580`, single scrolling column: nav in two columns at 18px (44px touch targets) including two extra items "How we work" and "Questions" (scroll to those sections with a 96px offset; "3D Models" scrolls to `#selected-concepts`), then the info text, then the two cards.
- Content sections: 20px side margins, max-width 640; titles 28px, `h3` 22px, body 15px. The process section is no longer pinned (auto height, order: title, visual, steps; all steps full opacity; tapping a step selects it and scrolls the visual into view). Work section becomes one column; the nav becomes a 2×2 grid of bordered text chips (active chip black with lime text). Section padding 72px.
- Marquee: `56px + safe-area` tall, 24px logos, 12px text.
- Landscape phones (≤900 wide and ≤500 tall): heading at top 96, width 40%, 19px.
- Narrow desktop windows (≤900 wide with a mouse, `hover: hover` and `pointer: fine`): the menu keeps its 24svh strip and shows only the two-column navigation at 13px; the info text and cards are hidden.

## 20. Reduced motion and accessibility

With `prefers-reduced-motion: reduce`: no preloader easing or fade, no glyph flicker or moving light (time held at 0), no parallax, no cursor trail, no scramble, no marquee, no CSS transitions in the menu or sections, instant scroll jumps, process section unpinned, camera values snap instead of easing.

Other requirements: the canvas stage has `role="img"` with an `aria-label` that changes between journey and showcase; the hidden showcase layout is `aria-hidden` until its copy is visible; focus rings are 2px outlines (lime on dark, `currentColor` in sections); the menu button exposes `aria-expanded` and `aria-haspopup="dialog"`; the progress steps use `aria-current="step"`.

## 21. Acceptance checks

- Loading shows a giant lime percentage on black that reaches 100, then fades to the ASCII ensemble with the five lime heading strips.
- Every glyph cell visibly flickers between neighbouring characters; a band of light drifts across the facades; the ground is a field of tiny dots.
- Moving the mouse over the model tilts it slightly and leaves fading lime glyphs.
- Scrolling: heading fades, camera dives in with no visible jump between orthographic and perspective, walks the passage behind a lime ASCII cube with three speech bubbles, then pulls back as the canvas shrinks into a centred square on solid lime with text on both sides and the header turning black.
- Further down: a pinned 4-step process whose right-hand panel switches live camera views, then a lime work section with four live ASCII panels and live thumbnails, then a lime FAQ accordion.
- No console errors; no network requests beyond the site's own files.
[/hidecontent]
← 返回首页