Inknomad
aadmin2026-10-26 发布未分类3 阅读

Inknomad

类型: · 收录:2026-10-26 23:39:21 · 源码:会员可见

[hidecontent type="vip1"]
# Prompt: recreate the "Cink Nomon" 3D game main menu

Copy everything below the line into a new Claude Code session (or any coding agent with web access and a shell).

---

Build a cyberpunk game main-menu web page called **Cink Nomon** that matches the reference exactly: light grey studio background, top nav (HOME / TASK / INKNOMAD / PROFILE / SETTINGS with small icons), a black "01 / CONTINUE" header block with a numbered menu list below it on the left (01 Continue, 02 New Game, 03 Load Game, 04 Settings, 05 Exit), a bordered player card on the right (ECHD, Lv. 23, XP bar 12,450 / 18,000, Location AED MARTIOR, Time 08:45:52, Difficulty HARD), a save-point slot bottom-left (Current mission UNDERGROUND PROTOCOL, Progress 68%, monochrome sea-stack thumbnail drawn on a canvas), a red soda can on the floor, and "⏎ CONFIRM / ESC BACK" prompts bottom-right. The character in the centre is a **real 3D mesh** rendered with Three.js, not an image: a crouching tattooed woman with a black chrome prosthetic left arm, black crop top and shorts, white belt, white socks and white gum-sole sneakers. She auto-rotates slowly, can be dragged to rotate, and casts a soft contact shadow on the white floor.

## Assets (already generated, download these; do not regenerate)

| Asset | URL |
|---|---|
| Textured GLB mesh (Meshy 7 via Higgsfield, 60k tris, PBR, 15.4 MB) | https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261012_055404_f6445442-6be8-46e6-a401-5b3fd0b6a098.glb |
| Background-removed character cutout PNG (poster shown while the mesh loads) | https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261012_055123_93a1bfd7-bbeb-433c-abfe-5bfcaca0520c.png |
| Original render (GPT Image 2.5 via Higgsfield, 2:3) | https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20261012_054952_51794d74-d9ab-4363-9552-12e6d301065f.png |

Save them as `echd.glb`, `echd.png`, and `echd-raw.png` next to the page.

If the assets ever need regenerating, this is the pipeline that produced them on Higgsfield:
1. `generate_image`, model `gpt_image_2_5`, aspect 2:3, prompt:
   > Full-body 3D game character render, photorealistic, of a young woman crouching low in a squat on a plain pure white studio floor, seamless white background, soft diffuse shadow under her. Messy dark curly hair pulled into a loose high ponytail with face-framing strands, hoop earrings, intense direct gaze at camera. Arms and legs covered in dense black-ink tattoo sleeves. Her left arm from the elbow down is a sleek black chrome cybernetic prosthetic with articulated fingers and a small glowing blue light. Outfit: black high-neck sleeveless crop top, black athletic shorts, white belt with silver buckle, white crew socks, white chunky running sneakers with tan gum soles. Elbows resting on knees, one hand hanging between her knees. Cyberpunk game main-menu character pose, centered, clean minimal aesthetic, high detail, ultra sharp, Unreal Engine look.
   (Do not say "sheer" or "mesh top": that version was blocked by the content filter.)
2. `remove_background` on the result (image).
3. `generate_3d`, model `meshy_v7_image_to_3d`, input = the cutout, `should_texture: true`, `enable_pbr: true`, `target_polycount: 60000`, `symmetry_mode: "off"` (the prosthetic is one-sided). Cost was 38 credits, ~5 minutes.

## Hosting note (Claude Artifacts only)

Claude's artifact host does not serve `.glb`. If publishing there, split the GLB into a JSON glTF with the geometry buffer inlined as a base64 data URI plus the three textures as separate `.png` / `.jpg` files, and load `echd.gltf.json` instead. This Python does it (no dependencies):

```python
import struct, json, base64
d = open('echd.glb','rb').read()
ln = struct.unpack('<I', d[12:16])[0]; j = json.loads(d[20:20+ln])
bl = struct.unpack('<I', d[20+ln:24+ln])[0]; binb = d[28+ln:28+ln+bl]
imgbv = {i['bufferView']: k for k, i in enumerate(j['images'])}
ext = {'image/png': 'png', 'image/jpeg': 'jpg'}
for k, i in enumerate(j['images']):
    bv = j['bufferViews'][i['bufferView']]; name = 'echd_tex%d.%s' % (k, ext[i['mimeType']])
    o = bv.get('byteOffset', 0); open(name, 'wb').write(binb[o:o+bv['byteLength']])
    i.clear(); i['uri'] = name
newbuf = bytearray(); remap = {}; newbvs = []
for k, bv in enumerate(j['bufferViews']):
    if k in imgbv: continue
    o = bv.get('byteOffset', 0); data = binb[o:o+bv['byteLength']]
    while len(newbuf) % 4: newbuf.append(0)
    nb = dict(bv); nb['byteOffset'] = len(newbuf); nb['buffer'] = 0
    remap[k] = len(newbvs); newbvs.append(nb); newbuf += data
j['bufferViews'] = newbvs
for a in j['accessors']:
    if 'bufferView' in a: a['bufferView'] = remap[a['bufferView']]
j['buffers'] = [{'byteLength': len(newbuf), 'uri': 'data:application/octet-stream;base64,' + base64.b64encode(bytes(newbuf)).decode()}]
open('echd.gltf.json', 'w').write(json.dumps(j))
```

Anywhere else (a normal static host), keep `echd.glb` and change the loader path in the page to `'echd.glb'`.

## Implementation requirements

- Single HTML file, no build step. Fonts: Rajdhani (display) and Share Tech Mono (mono) from Google Fonts.
- Three.js 0.128.0 UMD from jsDelivr: `build/three.min.js`, `examples/js/loaders/GLTFLoader.js`, `examples/js/controls/OrbitControls.js`.
- Renderer: antialias, alpha, sRGB output, ACES tone mapping (exposure 1.05), PCF soft shadows. Hemisphere light + a key directional light at (2.5, 5, 3) casting a 2048 px shadow + a cool rim light at (-3, 3, -3). Floor is a `ShadowMaterial` plane, opacity 0.22.
- Model is normalised to 1.7 units tall, feet on y=0, camera at FOV 28 looking at mid-height. OrbitControls: no pan, no zoom, damping, polar angle clamped to 0.35π–0.52π, autoRotate 0.6 (off under `prefers-reduced-motion`).
- The `<canvas>` is absolutely positioned and 190% wide (left/right −45%) so the cast shadow is not clipped by the middle grid column. **Size the WebGL drawing buffer from the canvas's own clientWidth/clientHeight, not the parent's**, or the render stretches. All UI panels get `position: relative; z-index: 2` so they stay clickable above the canvas. On phone widths the canvas returns to 100% width.
- The PNG cutout sits under the canvas as a poster and fades out when the GLTF loads (`.char.ready`).
- Menu: hovering, focusing, or ArrowUp/ArrowDown changes the active item and rewrites the black header block ("02 / NEW GAME" etc.).
- Light theme only, no dark mode. Page fits one screen on desktop (`overflow: hidden`), stacks to one column and scrolls on phones.

## Full page source (copy verbatim as `cink-nomon-menu.html`; it is written for Claude's artifact skeleton, so add `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">` at the top and `</head><body>` before `<div class="screen">` plus `</body></html>` at the end if serving it standalone)

```html
<title>Cink Nomon</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Share+Tech+Mono&display=swap">
<style>
/* Layout: a single full-bleed game screen. Character render centered, menu list anchored left,
   player card anchored right, top nav and bottom prompts pinned to the frame edges. */
:root {
  --bg: #f3f3f4;
  --bg-vignette: #e4e4e7;
  --fg: #111114;
  --muted: #7a7a82;
  --line: #c6c6cc;
  --ink: #0b0b0e;
  --ink-fg: #f4f4f6;
  --accent: #27b4ff;
  --xp: #1c1c21;
  --font-display: "Rajdhani", "Bahnschrift", "Arial Narrow", sans-serif;
  --font-mono: "Share Tech Mono", "SF Mono", Menlo, monospace;
  color-scheme: light only;
}
html, body { height: 100%; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font-family: var(--font-display); font-weight: 500; letter-spacing: .04em;
  overflow: hidden;
}
.screen {
  position: relative; height: 100%; min-height: 560px;
  padding-inline: clamp(16px, 4vw, 56px); padding-block: clamp(16px, 3vh, 32px);
  display: grid;
  grid-template-rows: auto 1fr auto;
  grid-template-columns: minmax(220px, 300px) 1fr minmax(200px, 240px);
  grid-template-areas: "brand nav nav" "menu char card" "save foot foot";
  gap: 16px;
  background: radial-gradient(ellipse 70% 60% at 50% 55%, var(--bg) 40%, var(--bg-vignette) 100%);
}
.mono { font-family: var(--font-mono); letter-spacing: .06em; }
.label { font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }

/* brand */
.brand { grid-area: brand; position: relative; z-index: 2; align-self: start; }
.brand h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; }
.brand .ver { font-family: var(--font-mono); font-size: 10px; color: var(--muted); letter-spacing: .2em; }

/* top nav */
.nav { grid-area: nav; position: relative; z-index: 2; display: flex; justify-content: end; gap: clamp(14px, 2.4vw, 36px); align-self: start; }
.nav a {
  color: var(--fg); text-decoration: none; font-size: 12px; font-weight: 700; letter-spacing: .14em;
  text-transform: uppercase; display: inline-flex; align-items: center; gap: 7px; padding-bottom: 5px;
  border-bottom: 2px solid transparent;
}
.nav a svg { width: 13px; height: 13px; fill: currentColor; }
.nav a[aria-current] { border-bottom-color: var(--fg); }
.nav a:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* menu */
.menu { grid-area: menu; position: relative; z-index: 2; align-self: center; display: flex; flex-direction: column; gap: 6px; }
.menu-head {
  background: var(--ink); color: var(--ink-fg); padding: 12px 18px; margin-bottom: 20px;
  display: flex; align-items: center; gap: 12px; position: relative;
  box-shadow: 6px 8px 0 color-mix(in srgb, var(--fg) 12%, transparent);
}
.menu-head .n { font-family: var(--font-mono); font-size: 18px; }
.menu-head .t { font-size: 22px; font-weight: 600; letter-spacing: .3em; text-transform: uppercase; }
.menu-head .dots { position: absolute; right: 10px; bottom: 4px; font-family: var(--font-mono); font-size: 9px; opacity: .6; }
.menu ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.menu button {
  all: unset; display: grid; grid-template-columns: 34px 14px 1fr; align-items: center;
  padding: 9px 10px; cursor: pointer; font: inherit; font-size: 14px; font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--fg); border-bottom: 1px solid transparent; transition: background .12s, padding-left .12s;
}
.menu button .n { font-family: var(--font-mono); font-size: 13px; color: var(--muted); }
.menu button .sl { color: var(--line); font-size: 11px; }
.menu li.active button, .menu button:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); padding-left: 16px; }
.menu li.active button { border-bottom-color: var(--line); }
.menu li.active button .n { color: var(--fg); border-bottom: 1px solid var(--fg); }
.menu button:focus-visible { outline: 2px solid var(--accent); }

/* character */
.char {
  grid-area: char; position: relative; min-width: 0; min-height: 360px; display: flex; justify-content: center; align-items: end;
}
.char canvas.gl { position: absolute; top: 0; bottom: 0; left: -45%; right: -45%; width: 190%; height: 100%; display: block; opacity: 0; transition: opacity .6s; cursor: grab; }
.char canvas.gl:active { cursor: grabbing; }
.char.ready canvas.gl { opacity: 1; }
.char.ready img { opacity: 0; }
.char img { transition: opacity .6s; }
.char .hint { position: absolute; left: 50%; bottom: 2%; transform: translateX(-50%); font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); pointer-events: none; opacity: 0; transition: opacity .6s .4s; white-space: nowrap; }
.char.ready .hint { opacity: 1; }
.char img, .char .placeholder {
  max-height: 100%; max-width: 100%; height: clamp(360px, 82vh, 820px); object-fit: contain;
  mix-blend-mode: multiply;
  filter: drop-shadow(-24px 36px 28px rgba(0,0,0,.22));
}
.char .placeholder {
  aspect-ratio: 2/3; width: auto; border: 1px dashed var(--line); display: grid; place-items: center;
  color: var(--muted); font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
}
.can { position: absolute; right: 12%; bottom: 8%; width: 46px; height: 26px; transform: rotate(-14deg); }

/* player card */
.card {
  grid-area: card; position: relative; z-index: 2; align-self: start; margin-top: clamp(24px, 14vh, 120px);
  border: 1px solid var(--line); padding: 16px 18px;
  clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 14px 100%, 0 calc(100% - 14px));
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  display: flex; flex-direction: column; gap: 16px;
}
.card .id { display: grid; grid-template-columns: 44px 1fr; gap: 12px; align-items: center; }
.card .id svg { width: 44px; height: 44px; }
.card .name { font-size: 18px; font-weight: 700; letter-spacing: .14em; }
.card .lvl { font-size: 13px; color: var(--muted); }
.xp { height: 3px; background: var(--line); margin-top: 6px; }
.xp i { display: block; height: 100%; width: 69%; background: var(--xp); }
.card hr { border: 0; border-top: 1px solid var(--line); margin: 0; }
.stat { display: grid; grid-template-columns: 1fr auto; row-gap: 2px; align-items: start; }
.stat .v { font-size: 13px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; grid-column: 1; }
.stat .ico { grid-column: 2; grid-row: 1 / 3; width: 14px; height: 14px; border: 1px solid var(--line); border-radius: 2px; }
.stats { display: flex; flex-direction: column; gap: 14px; }

/* save slot */
.save {
  grid-area: save; z-index: 2; align-self: end; border: 1px solid var(--line); display: grid; grid-template-columns: 112px 1fr;
  position: relative; padding: 0; background: color-mix(in srgb, var(--bg) 70%, transparent);
}
.save .tag { position: absolute; top: -8px; left: 8px; background: var(--bg); padding: 0 4px; font-family: var(--font-mono); font-size: 9px; color: var(--muted); letter-spacing: .12em; }
.save canvas { width: 112px; height: 78px; display: block; }
.save .body { padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.save .mission { font-size: 13px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.save .pct { font-family: var(--font-mono); font-size: 11px; }

/* footer prompts */
.foot { grid-area: foot; position: relative; z-index: 2; display: flex; justify-content: end; gap: 28px; align-self: end; }
.key { display: inline-flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
.key kbd { font-family: var(--font-mono); font-size: 10px; border: 1px solid var(--fg); padding: 1px 6px; border-radius: 2px; }

/* reveal */
@media (prefers-reduced-motion: no-preference) {
  .menu, .card, .save, .nav, .brand { animation: rise .5s cubic-bezier(.2,.8,.2,1) both; }
  .card { animation-delay: .12s } .save { animation-delay: .18s }
  @keyframes rise { from { opacity: .001; transform: translateY(10px) } to { opacity: 1; transform: none } }
}

/* phone */
@media (max-width: 760px) {
  body { overflow: auto; }
  .screen {
    height: auto; min-height: 100%;
    grid-template-columns: 1fr; grid-template-areas: "brand" "nav" "char" "menu" "card" "save" "foot";
  }
  .nav { justify-content: start; flex-wrap: wrap; }
  .char img, .char .placeholder { height: 60vh; }
  .char canvas.gl { left: 0; right: 0; width: 100%; }
  .card { margin-top: 0; }
  .foot { justify-content: start; }
}
</style>

<div class="screen">
  <header class="brand">
    <h1>Cink Nomon</h1>
    <div class="ver">VER.1.0.0</div>
  </header>

  <nav class="nav" aria-label="Sections">
    <a href="#home" aria-current="page"><svg viewBox="0 0 16 16"><path d="M8 1 1 7h2v7h4v-4h2v4h4V7h2z"/></svg>Home</a>
    <a href="#task"><svg viewBox="0 0 16 16"><path d="M2 2h12v3H2zm0 4.5h12v3H2zm0 4.5h12v3H2z"/></svg>Task</a>
    <a href="#inknomad"><svg viewBox="0 0 16 16"><path d="M3 1h10v14H3zm2 2v2h6V3z"/></svg>Inknomad</a>
    <a href="#profile"><svg viewBox="0 0 16 16"><path d="M8 1a4 4 0 1 1 0 8 4 4 0 0 1 0-8zm0 9c4 0 7 2 7 5H1c0-3 3-5 7-5z"/></svg>Profile</a>
    <a href="#settings"><svg viewBox="0 0 16 16"><path d="M8 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm-6 3 1.5-1 .2-1.8L5 4l1.8.4L8 3l1.2 1.4L11 4l1.3 1.2.2 1.8L14 8l-1.5 1-.2 1.8L11 12l-1.8-.4L8 13l-1.2-1.4L5 12l-1.3-1.2-.2-1.8z"/></svg>Settings</a>
  </nav>

  <section class="menu" aria-label="Main menu">
    <div class="menu-head" id="menu-head">
      <span class="n" id="head-n">01 /</span>
      <span class="t" id="head-t">Continue</span>
      <span class="dots">.. .</span>
    </div>
    <ul id="menu">
      <li class="active"><button type="button" data-i="01" data-t="Continue"><span class="n">01</span><span class="sl">//</span>Continue</button></li>
      <li><button type="button" data-i="02" data-t="New Game"><span class="n">02</span><span class="sl">//</span>New Game</button></li>
      <li><button type="button" data-i="03" data-t="Load Game"><span class="n">03</span><span class="sl">//</span>Load Game</button></li>
      <li><button type="button" data-i="04" data-t="Settings"><span class="n">04</span><span class="sl">//</span>Settings</button></li>
      <li><button type="button" data-i="05" data-t="Exit"><span class="n">05</span><span class="sl">//</span>Exit</button></li>
    </ul>
  </section>

  <figure class="char" aria-label="Echd, player character">
    <img id="hero" src="echd.png" alt="Echd crouching, cybernetic left arm, tattoos, white sneakers">
    <canvas class="gl" id="gl" aria-hidden="true"></canvas>
    <span class="hint">// DRAG TO ROTATE · 3D MESH</span>
    <svg class="can" viewBox="0 0 46 26" aria-hidden="true">
      <rect x="2" y="3" width="42" height="20" rx="9" fill="#c8102e"/>
      <rect x="2" y="3" width="42" height="20" rx="9" fill="url(#g)"/>
      <ellipse cx="43" cy="13" rx="3" ry="9.5" fill="#d8d8dc"/>
      <defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".3"/></linearGradient></defs>
    </svg>
  </figure>

  <aside class="card" aria-label="Player status">
    <div class="id">
      <svg viewBox="0 0 44 44" aria-hidden="true">
        <path d="M22 10 10 4l5 12-9 2 10 6-4 11 10-8 10 8-4-11 10-6-9-2 5-12z" fill="none" stroke="currentColor" stroke-width="1.3"/>
        <circle cx="22" cy="16" r="2" fill="var(--accent)"/>
      </svg>
      <div>
        <div class="name">ECHD</div>
        <div class="lvl">Lv. 23</div>
        <div class="xp"><i></i></div>
        <div class="mono" style="font-size:10px;color:var(--muted);margin-top:4px">XP 12,450 / 18,000</div>
      </div>
    </div>
    <hr>
    <div class="stats">
      <div class="stat"><span class="label">Location</span><span class="v">Aed Martior</span><span class="ico"></span></div>
      <div class="stat"><span class="label">Time</span><span class="v mono" style="text-transform:none">08:45:52</span><span class="ico"></span></div>
      <div class="stat"><span class="label">Difficulty</span><span class="v">Hard</span><span class="ico"></span></div>
    </div>
  </aside>

  <section class="save" aria-label="Current save">
    <span class="tag">// SAVE POINT</span>
    <canvas id="thumb" width="224" height="156" aria-hidden="true"></canvas>
    <div class="body">
      <span class="label">Current mission</span>
      <span class="mission">Underground Protocol</span>
      <span class="label">Progress</span>
      <span class="pct">68%</span>
    </div>
  </section>

  <footer class="foot">
    <span class="key"><kbd>⏎</kbd> Confirm</span>
    <span class="key"><kbd>ESC</kbd> Back</span>
  </footer>
</div>

<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
(function () {
  // 3D character viewer: Higgsfield/Meshy GLB in a white studio with a soft contact shadow
  if (!window.THREE || !THREE.GLTFLoader) return;
  var fig = document.querySelector('.char'), cv = document.getElementById('gl');
  var renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.05;
  renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  var scene = new THREE.Scene();
  var camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
  var controls = new THREE.OrbitControls(camera, cv);
  controls.enablePan = false; controls.enableZoom = false; controls.enableDamping = true;
  controls.minPolarAngle = Math.PI * 0.35; controls.maxPolarAngle = Math.PI * 0.52;
  controls.autoRotate = true; controls.autoRotateSpeed = 0.6;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (reduce) controls.autoRotate = false;

  scene.add(new THREE.HemisphereLight(0xffffff, 0xd8d8de, 1.1));
  var key = new THREE.DirectionalLight(0xffffff, 1.6); key.position.set(2.5, 5, 3); key.castShadow = true;
  key.shadow.mapSize.set(2048, 2048); key.shadow.radius = 6; key.shadow.bias = -0.0005; scene.add(key);
  var rim = new THREE.DirectionalLight(0xbfe6ff, 0.8); rim.position.set(-3, 3, -3); scene.add(rim);
  var floor = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.ShadowMaterial({ opacity: 0.22 }));
  floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);

  function resize() {
    var w = cv.clientWidth, h = cv.clientHeight; if (!w || !h) return;
    renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
  }
  window.addEventListener('resize', resize); resize();

  new THREE.GLTFLoader().load('echd.gltf.json', function (g) {
    var m = g.scene;
    m.traverse(function (o) { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; if (o.material && o.material.map) o.material.map.encoding = THREE.sRGBEncoding; } });
    var box = new THREE.Box3().setFromObject(m), size = box.getSize(new THREE.Vector3()), c = box.getCenter(new THREE.Vector3());
    var s = 1.7 / Math.max(size.x, size.y, size.z); m.scale.setScalar(s);
    box.setFromObject(m); c = box.getCenter(new THREE.Vector3());
    m.position.set(-c.x, -box.min.y, -c.z);
    scene.add(m);
    var h = box.max.y - box.min.y;
    camera.position.set(0, h * 0.55, h * 3.1); controls.target.set(0, h * 0.5, 0); controls.update();
    fig.classList.add('ready');
  }, undefined, function (err) { console.warn('GLB failed to load; keeping poster image', err); });

  (function loop() { requestAnimationFrame(loop); controls.update(); renderer.render(scene, camera); })();
})();
</script>
<script>
(function () {
  var items = Array.prototype.slice.call(document.querySelectorAll('#menu li'));
  var headN = document.getElementById('head-n'), headT = document.getElementById('head-t');
  var idx = 0;
  function select(i) {
    idx = (i + items.length) % items.length;
    items.forEach(function (li, k) { li.classList.toggle('active', k === idx); });
    var b = items[idx].querySelector('button');
    headN.textContent = b.dataset.i + ' /';
    headT.textContent = b.dataset.t;
  }
  items.forEach(function (li, k) {
    li.querySelector('button').addEventListener('mouseenter', function () { select(k); });
    li.querySelector('button').addEventListener('focus', function () { select(k); });
  });
  document.addEventListener('keydown', function (e) {
    if (e.key === 'ArrowDown') { select(idx + 1); e.preventDefault(); }
    if (e.key === 'ArrowUp') { select(idx - 1); e.preventDefault(); }
  });

  // save-point thumbnail: a monochrome sea-stack skyline drawn on canvas
  var c = document.getElementById('thumb'), g = c.getContext('2d');
  var W = c.width, H = c.height;
  var sky = g.createLinearGradient(0, 0, 0, H); sky.addColorStop(0, '#d9dadd'); sky.addColorStop(1, '#9a9ca2');
  g.fillStyle = sky; g.fillRect(0, 0, W, H);
  g.fillStyle = '#6c6e74'; g.fillRect(0, H * .72, W, H);
  function stack(x, w, h) {
    g.fillStyle = '#2e2f33'; g.beginPath();
    g.moveTo(x, H * .72); g.lineTo(x + w * .15, H * .72 - h); g.lineTo(x + w * .45, H * .72 - h * 1.1);
    g.lineTo(x + w * .7, H * .72 - h * .8); g.lineTo(x + w, H * .72); g.closePath(); g.fill();
  }
  stack(W * .22, W * .2, H * .42); stack(W * .4, W * .14, H * .5); stack(W * .55, W * .1, H * .3);
  g.fillStyle = 'rgba(255,255,255,.35)'; for (var i = 0; i < 40; i++) g.fillRect(Math.random() * W, H * .72 + Math.random() * H * .28, 6, 1);
  g.fillStyle = '#eee'; g.font = '9px monospace'; g.fillText('N 54°12′  E 10°06′', 6, H - 8);
})();
</script>
```
[/hidecontent]
← 返回首页