Files
nagi-bench/outputs/gpt-5-6-sol/codex-cli-ultra/skeleton-watch-calibre-01.html
T
Jesse ZhangandClaude Opus 4.8 64609bd6c8 feat: add GPT-5.6-Terra and alternate takes for GPT-5.6-Sol
New model gpt-5-6-terra (Codex CLI, ultra): mythos-craft, pelican-cycling,
skeleton-watch (one-shot, unmodified).

gpt-5-6-sol-ultra gains alternate one-shot variants for existing cases:
mythos-craft 'Block Era', two more pelican SVGs, and a cleanly completed
skeleton-watch 'Calibre 01' kept alongside the interruption-recovered
AURELIUS S-01. Extra variants attributed to Nagi-ovo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:09:15 +01:00

1609 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#050706">
<title>Calibre 01 — 镂空机械腕表</title>
<style>
:root {
--ink: #f2f3ec;
--muted: #949b93;
--faint: rgba(225, 235, 226, .10);
--line: rgba(219, 233, 223, .16);
--panel: rgba(9, 13, 11, .68);
--accent: #c7ff8b;
--accent-2: #69f1c5;
--gold: #d8b878;
--danger: #ff5a60;
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #050706;
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
background:
radial-gradient(circle at 50% 42%, transparent 0 20%, rgba(0,0,0,.18) 58%, rgba(0,0,0,.72) 100%),
linear-gradient(125deg, rgba(86, 122, 103, .07), transparent 35%, rgba(181, 143, 82, .035) 70%, transparent);
mix-blend-mode: screen;
}
body::after {
content: "";
position: fixed;
inset: -50%;
z-index: 3;
pointer-events: none;
opacity: .035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.75'/%3E%3C/svg%3E");
animation: grain .28s steps(2) infinite;
}
@keyframes grain {
0% { transform: translate3d(0, 0, 0); }
25% { transform: translate3d(2%, -1%, 0); }
50% { transform: translate3d(-1%, 2%, 0); }
75% { transform: translate3d(1%, 1%, 0); }
100% { transform: translate3d(-2%, -1%, 0); }
}
button { font: inherit; }
#stage {
position: fixed;
inset: 0;
z-index: 1;
}
#stage canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
outline: none;
}
#stage canvas:active { cursor: grabbing; }
.topbar {
position: fixed;
z-index: 10;
top: 0;
left: 0;
right: 0;
height: 92px;
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 26px 30px;
pointer-events: none;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
pointer-events: auto;
}
.brand-mark {
width: 36px;
height: 36px;
border: 1px solid rgba(231, 239, 230, .45);
border-radius: 50%;
display: grid;
place-items: center;
position: relative;
box-shadow: inset 0 0 0 4px rgba(255,255,255,.025), 0 0 24px rgba(154, 255, 198, .08);
}
.brand-mark::before,
.brand-mark::after {
content: "";
position: absolute;
width: 1px;
height: 12px;
background: var(--accent);
transform-origin: 50% 100%;
bottom: 50%;
box-shadow: 0 0 8px rgba(199,255,139,.8);
}
.brand-mark::before { transform: rotate(42deg); }
.brand-mark::after { height: 8px; transform: rotate(-58deg); }
.brand-title {
font-family: Georgia, "Times New Roman", serif;
font-size: 18px;
letter-spacing: .16em;
line-height: 1;
}
.brand-sub {
margin-top: 7px;
color: var(--muted);
font-size: 9px;
letter-spacing: .28em;
text-transform: uppercase;
}
.scene-meta {
display: flex;
align-items: center;
gap: 14px;
padding-top: 2px;
color: var(--muted);
font-size: 10px;
letter-spacing: .18em;
text-transform: uppercase;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 0 4px rgba(105,241,197,.08), 0 0 14px rgba(105,241,197,.65);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .45; transform: scale(.8); } }
.chapter {
position: fixed;
z-index: 8;
left: 30px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 12px;
writing-mode: vertical-rl;
color: rgba(220, 228, 220, .36);
font-size: 9px;
letter-spacing: .32em;
text-transform: uppercase;
pointer-events: none;
}
.chapter::before {
content: "";
width: 1px;
height: 46px;
background: linear-gradient(transparent, rgba(225,235,226,.42), transparent);
}
.panel {
position: fixed;
z-index: 12;
top: 104px;
right: 26px;
width: min(340px, calc(100vw - 52px));
max-height: calc(100vh - 202px);
overflow: auto;
padding: 22px;
border: 1px solid var(--line);
border-radius: 18px;
background: linear-gradient(145deg, rgba(17,22,19,.78), rgba(7,10,9,.56));
box-shadow: 0 22px 80px rgba(0,0,0,.34), inset 0 1px rgba(255,255,255,.04);
backdrop-filter: blur(20px) saturate(125%);
-webkit-backdrop-filter: blur(20px) saturate(125%);
transition: opacity .35s ease, transform .45s cubic-bezier(.2,.8,.2,1);
}
.panel.closed {
opacity: 0;
pointer-events: none;
transform: translateX(24px) scale(.97);
}
.panel-kicker {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--accent);
font-size: 9px;
letter-spacing: .26em;
text-transform: uppercase;
}
.panel-close {
border: 0;
background: transparent;
color: var(--muted);
padding: 0;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 19px;
line-height: 1;
}
.panel h1 {
margin: 20px 0 8px;
max-width: 270px;
font-family: Georgia, "Times New Roman", serif;
font-weight: 400;
font-size: clamp(24px, 3vw, 34px);
letter-spacing: -.025em;
line-height: 1.04;
}
.panel-lead {
margin: 0;
color: #b7bdb6;
font-size: 12px;
line-height: 1.75;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
margin: 21px 0;
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
background: var(--line);
}
.metric {
min-height: 68px;
padding: 12px 10px;
background: rgba(6,9,7,.76);
}
.metric-value {
color: var(--ink);
font-family: Georgia, "Times New Roman", serif;
font-size: 17px;
}
.metric-label {
margin-top: 6px;
color: #737a73;
font-size: 8px;
letter-spacing: .12em;
text-transform: uppercase;
white-space: nowrap;
}
.detail-list {
border-top: 1px solid var(--line);
margin-top: 4px;
}
.detail-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 11px 0;
border-bottom: 1px solid rgba(225,235,226,.075);
font-size: 10px;
}
.detail-row span:first-child { color: #747b74; }
.detail-row span:last-child { color: #d9ddd7; text-align: right; }
.focus-button {
width: 100%;
margin-top: 18px;
padding: 12px 16px;
border: 1px solid rgba(199,255,139,.27);
border-radius: 10px;
color: var(--accent);
background: linear-gradient(180deg, rgba(199,255,139,.09), rgba(199,255,139,.025));
font-size: 10px;
letter-spacing: .14em;
text-transform: uppercase;
cursor: pointer;
transition: background .25s, border-color .25s, transform .25s;
}
.focus-button:hover {
background: rgba(199,255,139,.13);
border-color: rgba(199,255,139,.5);
transform: translateY(-1px);
}
.toolbar-wrap {
position: fixed;
z-index: 15;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 10px;
}
.toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 6px;
border: 1px solid var(--line);
border-radius: 15px;
background: rgba(8,12,10,.72);
box-shadow: 0 16px 54px rgba(0,0,0,.36), inset 0 1px rgba(255,255,255,.04);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.tool-button {
position: relative;
height: 42px;
min-width: 42px;
padding: 0 12px;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: #9da49d;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: color .25s, border-color .25s, background .25s;
}
.tool-button svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none;
}
.tool-button span {
font-size: 9px;
letter-spacing: .12em;
text-transform: uppercase;
white-space: nowrap;
}
.tool-button:hover,
.tool-button.active {
color: var(--accent);
border-color: rgba(199,255,139,.18);
background: rgba(199,255,139,.065);
}
.separator { width: 1px; height: 22px; background: var(--line); margin: 0 2px; }
.info-toggle {
width: 46px;
height: 46px;
flex: 0 0 auto;
border: 1px solid var(--line);
border-radius: 50%;
color: var(--ink);
background: rgba(8,12,10,.72);
cursor: pointer;
backdrop-filter: blur(16px);
font-family: Georgia, serif;
font-style: italic;
font-size: 16px;
transition: border-color .25s, color .25s;
}
.info-toggle:hover { color: var(--accent); border-color: rgba(199,255,139,.35); }
.hint {
position: fixed;
z-index: 8;
left: 50%;
bottom: 86px;
transform: translateX(-50%);
color: rgba(218,225,217,.44);
font-size: 9px;
letter-spacing: .13em;
white-space: nowrap;
pointer-events: none;
transition: opacity .5s;
}
.hint.hide { opacity: 0; }
#tooltip {
position: fixed;
z-index: 20;
left: 0;
top: 0;
pointer-events: none;
opacity: 0;
transform: translate3d(-100px,-100px,0);
transition: opacity .15s;
}
#tooltip.visible { opacity: 1; }
.tooltip-box {
position: relative;
margin: 14px 0 0 14px;
min-width: 128px;
padding: 9px 12px 10px;
border: 1px solid rgba(199,255,139,.28);
border-radius: 8px;
background: rgba(7,10,8,.88);
box-shadow: 0 12px 36px rgba(0,0,0,.35);
backdrop-filter: blur(12px);
}
.tooltip-box::before {
content: "";
position: absolute;
width: 5px;
height: 5px;
top: -18px;
left: -18px;
border: 1px solid var(--accent);
border-radius: 50%;
box-shadow: 0 0 10px rgba(199,255,139,.7);
}
.tooltip-id {
color: var(--accent);
font-size: 8px;
letter-spacing: .2em;
text-transform: uppercase;
}
.tooltip-name { margin-top: 4px; color: var(--ink); font-size: 11px; }
#hotspots {
position: fixed;
inset: 0;
z-index: 9;
pointer-events: none;
}
.hotspot {
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
gap: 7px;
color: rgba(227,234,226,.72);
font-size: 8px;
letter-spacing: .13em;
text-transform: uppercase;
pointer-events: auto;
cursor: pointer;
transform: translate(-4px,-4px);
transition: opacity .2s, color .2s;
white-space: nowrap;
}
.hotspot::before {
content: "";
width: 7px;
height: 7px;
flex: 0 0 auto;
border: 1px solid currentColor;
border-radius: 50%;
background: rgba(5,7,6,.7);
box-shadow: 0 0 0 3px rgba(199,255,139,.045);
}
.hotspot::after {
content: "";
position: absolute;
left: 7px;
top: 3px;
width: 22px;
height: 1px;
background: currentColor;
opacity: .28;
}
.hotspot span {
margin-left: 20px;
padding: 5px 7px;
border-radius: 5px;
background: rgba(5,8,6,.54);
backdrop-filter: blur(8px);
}
.hotspot:hover { color: var(--accent); }
.hotspot.offscreen { opacity: 0; pointer-events: none; }
.loading {
position: fixed;
inset: 0;
z-index: 100;
display: grid;
place-items: center;
background: #050706;
transition: opacity .8s ease, visibility .8s;
}
.loading.done { opacity: 0; visibility: hidden; }
.loader {
width: min(360px, calc(100vw - 64px));
text-align: center;
}
.loader-ring {
position: relative;
width: 72px;
height: 72px;
margin: 0 auto 28px;
border: 1px solid rgba(225,235,226,.12);
border-radius: 50%;
}
.loader-ring::before {
content: "";
position: absolute;
inset: 7px;
border: 1px solid transparent;
border-top-color: var(--accent);
border-right-color: rgba(199,255,139,.24);
border-radius: 50%;
animation: spin 1.3s cubic-bezier(.6,.1,.4,.9) infinite;
}
.loader-ring::after {
content: "";
position: absolute;
inset: 23px;
border: 1px solid rgba(225,235,226,.25);
border-radius: 50%;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loader-title {
font-family: Georgia, serif;
font-size: 20px;
letter-spacing: .08em;
}
.loader-copy {
margin-top: 10px;
color: #747c74;
font-size: 9px;
letter-spacing: .2em;
text-transform: uppercase;
}
.loader-track {
height: 1px;
margin-top: 26px;
overflow: hidden;
background: rgba(225,235,226,.1);
}
.loader-track::before {
content: "";
display: block;
width: 42%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
animation: load 1.2s ease-in-out infinite;
}
@keyframes load { from { transform: translateX(-110%); } to { transform: translateX(330%); } }
.noscript {
position: fixed;
z-index: 200;
inset: 0;
display: grid;
place-items: center;
padding: 40px;
color: #ddd;
background: #050706;
text-align: center;
}
@media (max-width: 820px) {
.topbar { height: 76px; padding: 18px; }
.scene-meta, .chapter { display: none; }
.brand-title { font-size: 15px; }
.panel {
top: auto;
right: 12px;
bottom: 82px;
width: calc(100vw - 24px);
max-height: min(55vh, 520px);
padding: 18px;
}
.panel.closed { transform: translateY(18px) scale(.98); }
.toolbar-wrap { bottom: 12px; width: calc(100vw - 24px); justify-content: center; }
.toolbar { flex: 1; justify-content: space-around; }
.tool-button { min-width: 38px; height: 40px; padding: 0 8px; }
.tool-button span { display: none; }
.info-toggle { width: 44px; height: 44px; }
.hint { bottom: 70px; font-size: 8px; }
.hotspot span { display: none; }
}
@media (prefers-reduced-motion: reduce) {
body::after, .live-dot, .loader-ring::before, .loader-track::before { animation: none !important; }
* { scroll-behavior: auto !important; }
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.166.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.166.1/examples/jsm/"
}
}
</script>
</head>
<body>
<noscript><div class="noscript">需要启用 JavaScript 才能查看 3D 机械腕表。</div></noscript>
<div id="stage" aria-label="可旋转的 3D 镂空机械腕表"></div>
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true"></div>
<div>
<div class="brand-title">CALIBRE 01</div>
<div class="brand-sub">Openworked Manufacture</div>
</div>
</div>
<div class="scene-meta"><span class="live-dot"></span><span>机械运转中 · 28,800 vph</span></div>
</header>
<div class="chapter">01 · Anatomy of time</div>
<aside class="panel" id="panel" aria-live="polite">
<div class="panel-kicker">
<span id="panel-kicker">Manufacture Calibre</span>
<button class="panel-close" id="panel-close" aria-label="关闭信息面板">×</button>
</div>
<h1 id="panel-title">时间的机械建筑</h1>
<p class="panel-lead" id="panel-lead">由 248 枚零件组成的手动上链镂空机芯。拖动观察,悬停识别零件,点击锁定并探索其功能。</p>
<div class="metric-grid" id="metric-grid">
<div class="metric"><div class="metric-value">72 h</div><div class="metric-label">动力储存</div></div>
<div class="metric"><div class="metric-value">31</div><div class="metric-label">红宝石</div></div>
<div class="metric"><div class="metric-value">4 Hz</div><div class="metric-label">摆频</div></div>
</div>
<div class="detail-list" id="detail-list">
<div class="detail-row"><span>机芯直径</span><span>34.6 mm</span></div>
<div class="detail-row"><span>结构</span><span>手动上链 · 镂空</span></div>
<div class="detail-row"><span>表壳</span><span>钛金属 · 蓝宝石水晶</span></div>
<div class="detail-row"><span>显示</span><span>时 · 分 · 小秒</span></div>
</div>
<button class="focus-button" id="focus-button">聚焦当前部件</button>
</aside>
<div id="tooltip">
<div class="tooltip-box">
<div class="tooltip-id" id="tooltip-id">Part 01</div>
<div class="tooltip-name" id="tooltip-name">部件</div>
</div>
</div>
<div id="hotspots" aria-hidden="true"></div>
<div class="hint" id="hint">拖动旋转 · 滚轮缩放 · 悬停识别 · 点击锁定</div>
<div class="toolbar-wrap">
<div class="toolbar" role="toolbar" aria-label="3D 腕表控制">
<button class="tool-button active" id="rotate-button" aria-pressed="true" title="自动旋转">
<svg viewBox="0 0 24 24"><path d="M20 7v5h-5"/><path d="M19 12a7 7 0 1 1-2.05-4.95L20 10"/></svg><span>旋转</span>
</button>
<button class="tool-button" id="explode-button" aria-pressed="false" title="爆炸视图">
<svg viewBox="0 0 24 24"><path d="m12 3 7 4-7 4-7-4 7-4Z"/><path d="m5 12 7 4 7-4"/><path d="m5 17 7 4 7-4"/></svg><span>爆炸</span>
</button>
<button class="tool-button" id="label-button" aria-pressed="false" title="部件标签">
<svg viewBox="0 0 24 24"><path d="M20 13 13 20l-9-9V4h7l9 9Z"/><path d="M8.5 8.5h.01"/></svg><span>标签</span>
</button>
<button class="tool-button" id="lume-button" aria-pressed="false" title="夜光模式">
<svg viewBox="0 0 24 24"><path d="M21 12.8A8.2 8.2 0 1 1 11.2 3 6.4 6.4 0 0 0 21 12.8Z"/></svg><span>夜光</span>
</button>
<div class="separator"></div>
<button class="tool-button" id="reset-button" title="重置视角">
<svg viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg><span>复位</span>
</button>
</div>
<button class="info-toggle" id="info-toggle" aria-label="显示机芯信息">i</button>
</div>
<div class="loading" id="loading">
<div class="loader">
<div class="loader-ring"></div>
<div class="loader-title">CALIBRE 01</div>
<div class="loader-copy">正在组装 248 枚精密零件</div>
<div class="loader-track"></div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const stage = document.getElementById('stage');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const isTouch = matchMedia('(pointer: coarse)').matches;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050706);
scene.fog = new THREE.FogExp2(0x050706, 0.022);
const camera = new THREE.PerspectiveCamera(32, innerWidth / innerHeight, 0.1, 80);
camera.position.set(0.2, 1.1, isTouch ? 25 : 22);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.16;
renderer.outputColorSpace = THREE.SRGBColorSpace;
stage.appendChild(renderer.domElement);
const pmrem = new THREE.PMREMGenerator(renderer);
const room = new RoomEnvironment();
scene.environment = pmrem.fromScene(room, 0.04).texture;
room.dispose();
pmrem.dispose();
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.52, 0.75, 0.72);
composer.addPass(bloom);
composer.addPass(new OutputPass());
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.minDistance = 12;
controls.maxDistance = 34;
controls.minPolarAngle = 0.35;
controls.maxPolarAngle = Math.PI - 0.35;
controls.autoRotate = !reduceMotion;
controls.autoRotateSpeed = 0.42;
controls.target.set(0, 0, 0.8);
const mat = {
titanium: new THREE.MeshPhysicalMaterial({ color: 0x5f6865, metalness: 1, roughness: 0.22, clearcoat: 0.48, clearcoatRoughness: 0.18 }),
darkTitanium: new THREE.MeshPhysicalMaterial({ color: 0x151b19, metalness: 0.93, roughness: 0.24, clearcoat: 0.34 }),
blackMetal: new THREE.MeshStandardMaterial({ color: 0x090c0b, metalness: 0.88, roughness: 0.3 }),
steel: new THREE.MeshPhysicalMaterial({ color: 0xb9c0ba, metalness: 1, roughness: 0.17, clearcoat: 0.35 }),
brushedSteel: new THREE.MeshStandardMaterial({ color: 0x89918c, metalness: 1, roughness: 0.38 }),
gold: new THREE.MeshPhysicalMaterial({ color: 0xb9904c, metalness: 1, roughness: 0.2, clearcoat: 0.45 }),
roseGold: new THREE.MeshPhysicalMaterial({ color: 0xb27255, metalness: 1, roughness: 0.22, clearcoat: 0.42 }),
brass: new THREE.MeshPhysicalMaterial({ color: 0x9e7737, metalness: 1, roughness: 0.3, clearcoat: 0.28 }),
blueSteel: new THREE.MeshPhysicalMaterial({ color: 0x153f75, metalness: 1, roughness: 0.17, clearcoat: 0.62 }),
ruby: new THREE.MeshPhysicalMaterial({ color: 0x9c061f, emissive: 0x2a0007, emissiveIntensity: 1.4, metalness: 0.05, roughness: 0.12, transmission: 0.22, thickness: 0.32, ior: 2.1, clearcoat: 1 }),
lume: new THREE.MeshStandardMaterial({ color: 0xc9ff9d, emissive: 0x79ffba, emissiveIntensity: 2.6, roughness: 0.28, metalness: 0.03 }),
glass: new THREE.MeshPhysicalMaterial({ color: 0xc9fff2, metalness: 0, roughness: 0.035, transmission: 0.96, thickness: 0.28, ior: 1.77, transparent: true, opacity: 0.24, clearcoat: 1, clearcoatRoughness: 0 }),
smokedGlass: new THREE.MeshPhysicalMaterial({ color: 0x24322d, metalness: 0.05, roughness: 0.2, transmission: 0.6, thickness: 0.38, transparent: true, opacity: 0.5, side: THREE.DoubleSide }),
strap: new THREE.MeshPhysicalMaterial({ color: 0x080b0a, roughness: 0.66, metalness: 0.05, clearcoat: 0.12, sheen: 0.35, sheenColor: new THREE.Color(0x56645d) })
};
const lights = [];
const key = new THREE.SpotLight(0xe7fff0, 1050, 45, Math.PI * 0.18, 0.55, 1.2);
key.position.set(-9, 12, 16);
key.target.position.set(0, 0, 0);
key.castShadow = true;
key.shadow.mapSize.set(2048, 2048);
key.shadow.bias = -0.00015;
scene.add(key, key.target); lights.push(key);
const warm = new THREE.SpotLight(0xffc987, 760, 40, Math.PI * 0.22, 0.65, 1.4);
warm.position.set(12, 5, 10);
warm.target.position.set(0, 0, 0);
scene.add(warm, warm.target); lights.push(warm);
const rim = new THREE.SpotLight(0x77ffd3, 920, 36, Math.PI * 0.2, 0.7, 1.25);
rim.position.set(-8, -8, 9);
rim.target.position.set(0, 0, 0);
scene.add(rim, rim.target); lights.push(rim);
const topLight = new THREE.PointLight(0xffffff, 75, 18, 2);
topLight.position.set(0, 7, 7);
scene.add(topLight); lights.push(topLight);
scene.add(new THREE.HemisphereLight(0x8eafa2, 0x17100b, 0.55));
const watch = new THREE.Group();
watch.rotation.set(-0.08, 0.02, -0.055);
scene.add(watch);
const caseGroup = new THREE.Group();
const movement = new THREE.Group();
const dial = new THREE.Group();
const display = new THREE.Group();
watch.add(caseGroup, movement, dial, display);
const interactives = [];
const parts = new Map();
const animatedGears = [];
const hotspotIds = ['barrel', 'gear-train', 'balance', 'escapement', 'bridges', 'jewels', 'sapphire', 'display'];
function cloneInteractiveMaterials(root) {
root.traverse(child => {
if (!child.isMesh) return;
if (Array.isArray(child.material)) child.material = child.material.map(m => m.clone());
else child.material = child.material.clone();
const materials = Array.isArray(child.material) ? child.material : [child.material];
for (const m of materials) {
m.userData.baseEmissive = m.emissive ? m.emissive.clone() : null;
m.userData.baseEmissiveIntensity = m.emissiveIntensity ?? 1;
}
});
}
function registerPart(root, data, explode = 0) {
cloneInteractiveMaterials(root);
root.userData.basePosition = root.position.clone();
root.userData.explode = explode;
root.userData.partId = data.id;
root.traverse(child => {
if (child.isMesh) {
child.userData.partId = data.id;
interactives.push(child);
child.castShadow = true;
child.receiveShadow = true;
}
});
parts.set(data.id, { root, data });
return root;
}
function roundedRectShape(width, height, radius) {
const x = -width / 2, y = -height / 2;
const s = new THREE.Shape();
s.moveTo(x + radius, y);
s.lineTo(x + width - radius, y);
s.quadraticCurveTo(x + width, y, x + width, y + radius);
s.lineTo(x + width, y + height - radius);
s.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
s.lineTo(x + radius, y + height);
s.quadraticCurveTo(x, y + height, x, y + height - radius);
s.lineTo(x, y + radius);
s.quadraticCurveTo(x, y, x + radius, y);
return s;
}
function extrudedShape(shape, depth, material, bevel = 0.05) {
const geo = new THREE.ExtrudeGeometry(shape, { depth, bevelEnabled: true, bevelSegments: 2, bevelSize: bevel, bevelThickness: bevel, curveSegments: 24 });
geo.center();
geo.computeVertexNormals();
return new THREE.Mesh(geo, material);
}
function cylinder(radius, depth, material, segments = 64) {
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, depth, segments, 1, false), material);
mesh.rotation.x = Math.PI / 2;
return mesh;
}
function ring(radius, tube, material, radial = 96) {
return new THREE.Mesh(new THREE.TorusGeometry(radius, tube, 12, radial), material);
}
function screw(radius = 0.15, depth = 0.12, blue = true) {
const g = new THREE.Group();
const head = cylinder(radius, depth, blue ? mat.blueSteel : mat.steel, 32);
g.add(head);
const slotMat = new THREE.MeshBasicMaterial({ color: blue ? 0x081325 : 0x262b29 });
const slot = new THREE.Mesh(new THREE.BoxGeometry(radius * 1.35, radius * .22, depth * .22), slotMat);
slot.position.z = depth * .55;
g.add(slot);
return g;
}
function jewel(radius = 0.18) {
const g = new THREE.Group();
const setting = ring(radius * 1.15, radius * .16, mat.gold, 36);
const stone = cylinder(radius, .11, mat.ruby, 32);
stone.position.z = .025;
g.add(setting, stone);
return g;
}
function gearShape(radius, teeth, toothDepth, holeRadius) {
const s = new THREE.Shape();
const count = teeth * 4;
for (let i = 0; i <= count; i++) {
const a = (i / count) * Math.PI * 2;
const phase = i % 4;
const r = phase === 1 || phase === 2 ? radius + toothDepth : radius;
const x = Math.cos(a) * r, y = Math.sin(a) * r;
i === 0 ? s.moveTo(x, y) : s.lineTo(x, y);
}
const hole = new THREE.Path();
hole.absarc(0, 0, holeRadius, 0, Math.PI * 2, true);
s.holes.push(hole);
return s;
}
function createGear({ radius, teeth, depth = .16, material = mat.brass, spokes = 5, hub = .2, toothDepth = .11 }) {
const g = new THREE.Group();
const wheel = extrudedShape(gearShape(radius, teeth, toothDepth, radius * .29), depth, material, .025);
g.add(wheel);
const spokeMaterial = material.clone();
spokeMaterial.roughness = Math.min(1, (material.roughness || .2) + .08);
for (let i = 0; i < spokes; i++) {
const a = i / spokes * Math.PI * 2;
const len = radius * .66;
const spoke = new THREE.Mesh(new THREE.BoxGeometry(len, Math.max(.055, radius * .075), depth * .72), spokeMaterial);
spoke.position.set(Math.cos(a) * radius * .33, Math.sin(a) * radius * .33, 0);
spoke.rotation.z = a;
g.add(spoke);
}
g.add(cylinder(hub, depth * 1.35, material, 32));
const pinion = cylinder(hub * .48, depth * 2.8, mat.steel, 24);
pinion.position.z = depth * .55;
g.add(pinion);
return g;
}
function spiralTube(inner, outer, turns, depth, material, thickness = .025, points = 220) {
const p = [];
for (let i = 0; i <= points; i++) {
const t = i / points;
const a = t * turns * Math.PI * 2;
const r = inner + (outer - inner) * t;
p.push(new THREE.Vector3(Math.cos(a) * r, Math.sin(a) * r, depth + Math.sin(t * Math.PI) * .015));
}
const curve = new THREE.CatmullRomCurve3(p);
return new THREE.Mesh(new THREE.TubeGeometry(curve, points, thickness, 5, false), material);
}
function bridge(width, height, angle, x, y, z, material, holes = []) {
const s = roundedRectShape(width, height, Math.min(height * .42, .24));
for (const h of holes) {
const cut = new THREE.Path();
cut.absarc(h.x, h.y, h.r, 0, Math.PI * 2, true);
s.holes.push(cut);
}
const mesh = extrudedShape(s, .2, material, .055);
mesh.position.set(x, y, z);
mesh.rotation.z = angle;
return mesh;
}
function canvasTexture(draw, width = 1024, height = 256) {
const c = document.createElement('canvas');
c.width = width; c.height = height;
const ctx = c.getContext('2d');
draw(ctx, width, height);
const t = new THREE.CanvasTexture(c);
t.colorSpace = THREE.SRGBColorSpace;
t.anisotropy = renderer.capabilities.getMaxAnisotropy();
return t;
}
function addEngraving(text, x, y, z, width = 2.7, rotation = 0) {
const tex = canvasTexture((ctx, w, h) => {
ctx.clearRect(0,0,w,h);
ctx.font = '500 56px Georgia';
ctx.letterSpacing = '12px';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(233,221,186,.78)';
ctx.fillText(text, w/2, h/2);
});
const plane = new THREE.Mesh(new THREE.PlaneGeometry(width, width / 4), new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthWrite: false, opacity: .82, blending: THREE.AdditiveBlending }));
plane.position.set(x,y,z);
plane.rotation.z = rotation;
movement.add(plane);
}
// Strap and sculpted case
const strapTop = extrudedShape(roundedRectShape(4.35, 7.8, .72), .62, mat.strap, .11);
strapTop.position.set(0, 9.15, -1.0);
strapTop.rotation.x = -.045;
const strapBottom = strapTop.clone();
strapBottom.material = mat.strap.clone();
strapBottom.position.y = -9.15;
strapBottom.rotation.x = .045;
caseGroup.add(strapTop, strapBottom);
for (const sign of [-1, 1]) {
for (let i = -1; i <= 1; i++) {
const stitchTop = new THREE.Mesh(new THREE.BoxGeometry(.34, .035, .018), new THREE.MeshBasicMaterial({ color: 0x4d5550 }));
stitchTop.position.set(i * 1.25, sign > 0 ? 7.2 + sign * .25 : -7.2 + sign * .25, -.65);
caseGroup.add(stitchTop);
}
}
for (const y of [-5.8, 5.8]) {
for (const x of [-4.35, 4.35]) {
const lug = extrudedShape(roundedRectShape(1.05, 2.2, .36), .85, mat.titanium, .12);
lug.position.set(x, y, -.25);
lug.rotation.z = x * y > 0 ? -.19 : .19;
caseGroup.add(lug);
const lugInset = extrudedShape(roundedRectShape(.54, 1.65, .22), .91, mat.blackMetal, .06);
lugInset.position.copy(lug.position);
lugInset.position.z += .03;
lugInset.rotation.z = lug.rotation.z;
caseGroup.add(lugInset);
}
}
const caseBack = cylinder(6.28, .64, mat.darkTitanium, 128);
caseBack.position.z = -.43;
caseGroup.add(caseBack);
const exhibitionBack = cylinder(5.58, .68, mat.smokedGlass, 128);
exhibitionBack.position.z = -.45;
caseGroup.add(exhibitionBack);
const caseRing = ring(6.22, .52, mat.titanium, 160);
caseRing.position.z = .35;
caseGroup.add(caseRing);
const bezelOuter = ring(6.18, .23, mat.darkTitanium, 160);
bezelOuter.position.z = 2.62;
caseGroup.add(bezelOuter);
const bezelLip = ring(5.93, .08, mat.steel, 160);
bezelLip.position.z = 2.8;
caseGroup.add(bezelLip);
// Crown with knurling and crown guards
const crown = new THREE.Group();
crown.position.set(6.8, .12, .45);
const crownCore = cylinder(.62, .62, mat.titanium, 48);
crownCore.rotation.set(0,0,Math.PI/2);
crown.add(crownCore);
for (let i = 0; i < 24; i++) {
const a = i / 24 * Math.PI * 2;
const tooth = new THREE.Mesh(new THREE.BoxGeometry(.6, .045, .12), mat.steel);
tooth.position.set(0, Math.cos(a) * .61, Math.sin(a) * .61);
tooth.rotation.x = a;
crown.add(tooth);
}
const crownCap = cylinder(.45, .08, mat.darkTitanium, 48);
crownCap.rotation.set(0,0,Math.PI/2);
crownCap.position.x = .34;
crown.add(crownCap);
const crownMark = ring(.19, .035, mat.lume, 40);
crownMark.rotation.y = Math.PI/2;
crownMark.position.x = .39;
crown.add(crownMark);
caseGroup.add(crown);
for (const y of [-.78, .95]) {
const guard = extrudedShape(roundedRectShape(.74, 1.15, .26), .65, mat.titanium, .1);
guard.position.set(6.26, y, .2);
guard.rotation.z = .08 * Math.sign(y);
caseGroup.add(guard);
}
registerPart(caseGroup, {
id: 'case', code: 'Exterior 01', name: '钛金属表壳',
lead: '多层喷砂与镜面倒角构成的轻量化表壳,保护机芯并形成鲜明的光影边界。',
metrics: [['Grade 5','钛金属'],['43 mm','直径'],['10 ATM','防水']],
details: [['表面','拉丝 · 抛光倒角'],['结构','四体式表壳'],['表冠','螺旋防滑纹'],['重量','约 78 g']]
}, 0);
// Main plate and architectural rings
const mainPlate = new THREE.Group();
const plate = cylinder(5.48, .3, mat.smokedGlass, 128);
plate.position.z = .18;
mainPlate.add(plate);
const plateRing = ring(5.14, .14, mat.brushedSteel, 128);
plateRing.position.z = .36;
mainPlate.add(plateRing);
const innerRing = ring(4.15, .055, mat.gold, 128);
innerRing.position.z = .39;
mainPlate.add(innerRing);
for (let r = 1.15; r < 5; r += .64) {
const groove = ring(r, .009, new THREE.MeshBasicMaterial({ color: 0x6b746e, transparent: true, opacity: .24 }), 96);
groove.position.z = .38;
mainPlate.add(groove);
}
movement.add(mainPlate);
// Mainspring barrel
const barrel = new THREE.Group();
barrel.position.set(-2.55, 1.62, .76);
const barrelTeeth = createGear({ radius: 1.36, teeth: 46, depth: .2, material: mat.brass, spokes: 6, hub: .25, toothDepth: .09 });
barrel.add(barrelTeeth);
const barrelWall = ring(1.05, .09, mat.roseGold, 80);
barrelWall.position.z = .18;
barrel.add(barrelWall);
const barrelSpring = spiralTube(.12, .92, 7.2, .23, mat.blueSteel, .028, 260);
barrel.add(barrelSpring);
const barrelHub = cylinder(.26, .38, mat.steel, 40);
barrelHub.position.z = .16;
barrel.add(barrelHub);
const barrelScrew = screw(.13,.1,true);
barrelScrew.position.set(0,0,.39);
barrel.add(barrelScrew);
movement.add(registerPart(barrel, {
id:'barrel', code:'Energy 02', name:'开放式主发条盒',
lead:'可见的蓝钢主发条缓慢释放储存能量,经由发条盒齿圈传向走时轮系。',
metrics:[['72 h','动力储存'],['7.2','发条圈数'],['0.11 mm','簧片厚度']],
details:[['功能','储存与稳定释放能量'],['材质','Nivaflex 合金'],['上链','手动双向'],['安全结构','滑动末端']]
}, 3.2));
animatedGears.push({ object: barrelTeeth, speed: .018 });
// Gear train, including a deep background wheel for layered parallax
const gearTrain = new THREE.Group();
const gearSpecs = [
{ x:-.35, y:.55, r:1.05, teeth:38, color:mat.gold, speed:-.11, z:.82, spokes:5 },
{ x:1.62, y:1.45, r:.86, teeth:32, color:mat.brass, speed:.21, z:.76, spokes:5 },
{ x:2.82, y:-.05, r:.71, teeth:28, color:mat.roseGold, speed:-.36, z:.9, spokes:5 },
{ x:1.62, y:-1.47, r:.79, teeth:30, color:mat.gold, speed:.5, z:.73, spokes:6 },
{ x:-.3, y:-1.4, r:.68, teeth:26, color:mat.brass, speed:-.72, z:.62, spokes:5 },
{ x:3.63, y:1.8, r:.4, teeth:18, color:mat.steel, speed:-.72, z:.65, spokes:4 },
{ x:-3.65, y:-.5, r:.48, teeth:20, color:mat.steel, speed:.52, z:.6, spokes:4 }
];
gearSpecs.forEach((s, index) => {
const g = createGear({ radius:s.r, teeth:s.teeth, depth:.13 + index % 2 * .03, material:s.color, spokes:s.spokes, hub:s.r*.2, toothDepth:Math.max(.055,s.r*.095) });
g.position.set(s.x,s.y,s.z);
g.rotation.z = index * .31;
gearTrain.add(g);
animatedGears.push({ object:g, speed:s.speed });
});
// Pinion stacks communicate real depth and gearing
[[-.35,.55],[1.62,1.45],[2.82,-.05],[1.62,-1.47]].forEach(([x,y],i) => {
const pinion = createGear({ radius:.27, teeth:12, depth:.23, material: i%2 ? mat.steel : mat.blueSteel, spokes:4, hub:.08, toothDepth:.055 });
pinion.position.set(x,y,1.03);
gearTrain.add(pinion);
animatedGears.push({ object:pinion, speed:(i%2?-1:1)*(1.2+i*.3) });
});
movement.add(registerPart(gearTrain, {
id:'gear-train', code:'Transmission 03', name:'五级走时轮系',
lead:'不同齿数的轮与小齿轴逐级传递扭矩,把发条盒的缓慢转动转换成精准的秒脉冲。',
metrics:[['5','传动级'],['0.4 μm','齿面公差'],['14 K','金质轮']],
details:[['组成','中心轮 · 三轮 · 四轮'],['修饰','太阳纹与环形纹'],['轴榫','镜面抛光钢'],['啮合','渐开线齿形']]
}, 4.2));
// Balance wheel and breathing hairspring
const balance = new THREE.Group();
balance.position.set(-2.55,-2.66,1.02);
const balanceWheel = new THREE.Group();
const balanceRim = ring(1.19,.10,mat.roseGold,96);
balanceWheel.add(balanceRim);
for(let i=0;i<3;i++){
const a=i*Math.PI*2/3;
const spoke=new THREE.Mesh(new THREE.BoxGeometry(1.07,.085,.11),mat.roseGold);
spoke.position.set(Math.cos(a)*.53,Math.sin(a)*.53,0);
spoke.rotation.z=a;
balanceWheel.add(spoke);
}
for(let i=0;i<8;i++){
const a=i*Math.PI/4;
const weight=cylinder(.095,.12,mat.gold,24);
weight.position.set(Math.cos(a)*1.18,Math.sin(a)*1.18,.02);
balanceWheel.add(weight);
}
balanceWheel.add(cylinder(.18,.28,mat.steel,36));
balance.add(balanceWheel);
const hairspring = spiralTube(.08,.82,9.2,.09,mat.blueSteel,.018,300);
balance.add(hairspring);
const stud = cylinder(.08,.24,mat.gold,20); stud.position.set(.84,.04,.1); balance.add(stud);
balance.add(jewel(.18));
movement.add(registerPart(balance, {
id:'balance', code:'Regulation 04', name:'摆轮与游丝',
lead:'无卡度摆轮每秒往复八次;蓝钢游丝如同机芯的呼吸,决定腕表的时间基准。',
metrics:[['4 Hz','振动频率'],['28,800','每小时振次'],['8','微调砝码']],
details:[['摆轮','可变惯量三臂摆轮'],['游丝','平面式蓝钢合金'],['抗磁','约 15,000 高斯'],['调校','五方位调校']]
}, 5.5));
// Escapement wheel, pallet fork and impulse jewel
const escapement = new THREE.Group();
escapement.position.set(.15,-2.78,1.18);
const escapeWheel = createGear({radius:.58,teeth:20,depth:.11,material:mat.steel,spokes:5,hub:.11,toothDepth:.12});
escapeWheel.position.x=.47;
escapement.add(escapeWheel);
animatedGears.push({object:escapeWheel,speed:1.35,stepped:true});
const forkShape = new THREE.Shape();
forkShape.moveTo(-.72,-.16); forkShape.lineTo(-.2,-.08); forkShape.lineTo(.05,-.4);
forkShape.lineTo(.2,-.32); forkShape.lineTo(.05,.04); forkShape.lineTo(.21,.38);
forkShape.lineTo(.04,.44); forkShape.lineTo(-.22,.13); forkShape.lineTo(-.72,.2); forkShape.closePath();
const fork = extrudedShape(forkShape,.12,mat.roseGold,.02);
fork.position.x=-.3;
escapement.add(fork);
const pallet1 = new THREE.Mesh(new THREE.BoxGeometry(.24,.11,.14),mat.ruby); pallet1.position.set(.06,.37,.08); pallet1.rotation.z=-.35;
const pallet2 = pallet1.clone(); pallet2.material=mat.ruby.clone(); pallet2.position.set(.06,-.35,.08); pallet2.rotation.z=.35;
escapement.add(pallet1,pallet2);
const impulse = jewel(.11); impulse.position.set(-.76,0,.1); escapement.add(impulse);
movement.add(registerPart(escapement, {
id:'escapement', code:'Impulse 05', name:'瑞士杠杆擒纵',
lead:'擒纵轮与红宝石叉瓦交替锁止,将连续能量切分为稳定脉冲并维持摆轮振荡。',
metrics:[['20','擒纵齿'],['2','红宝石叉瓦'],['8 Hz','锁止脉冲']],
details:[['类型','瑞士杠杆式'],['擒纵轮','镍磷合金'],['叉瓦','合成红宝石'],['润滑','微量定点润滑']]
}, 6.2));
// Skeleton bridges with polished bevels, screws and engraving
const bridges = new THREE.Group();
const bridgePieces = [
bridge(3.3,.62,-.18,1.72,2.66,1.55,mat.roseGold,[{x:1.05,y:0,r:.17},{x:-1.08,y:0,r:.15}]),
bridge(3.85,.56,.46,.5,-.55,1.65,mat.brushedSteel,[{x:1.5,y:0,r:.16},{x:-1.5,y:0,r:.13}]),
bridge(2.2,.52,-.52,-3.15,-1.68,1.63,mat.roseGold,[{x:-.76,y:0,r:.14}]),
bridge(2.45,.45,.82,3.0,.8,1.5,mat.brushedSteel,[{x:.88,y:0,r:.12}])
];
bridgePieces.forEach(p=>bridges.add(p));
const bridgeBosses=[[-.06,2.39],[3.0,2.33],[-3.77,-1.14],[-2.44,-2.2],[1.8,.1],[3.76,1.55]];
bridgeBosses.forEach(([x,y],i)=>{
const boss=cylinder(.24,.2,i%2?mat.roseGold:mat.brushedSteel,36); boss.position.set(x,y,1.58); bridges.add(boss);
const s=screw(.115,.1,i%3!==0); s.position.set(x,y,1.72); bridges.add(s);
});
movement.add(registerPart(bridges, {
id:'bridges', code:'Architecture 06', name:'镂空夹板系统',
lead:'以最少覆盖支撑整套轮系。手工抛光内角、拉丝表面与镜面倒角在光线下清晰分层。',
metrics:[['4','独立夹板'],['42','手工倒角'],['0.2 mm','夹板厚度']],
details:[['材质','钛金属与玫瑰金'],['倒角','手工镜面抛光'],['表面','直纹拉丝'],['固定','热蓝钢螺钉']]
}, 7.0));
addEngraving('CAL. 01 · THIRTY-ONE JEWELS',.72,2.7,1.68,2.55,-.18);
// Ruby bearing array
const jewels = new THREE.Group();
const jewelPositions=[[-.35,.55,1.38],[1.62,1.45,1.32],[2.82,-.05,1.42],[1.62,-1.47,1.29],[-.3,-1.4,1.2],[-2.55,1.62,1.39],[-2.55,-2.66,1.26],[.63,-2.78,1.38],[3.62,1.8,1.1],[-3.64,-.5,1.08]];
jewelPositions.forEach(([x,y,z],i)=>{const j=jewel(i<8?.14:.11);j.position.set(x,y,z);jewels.add(j);});
movement.add(registerPart(jewels, {
id:'jewels', code:'Friction 07', name:'红宝石轴承',
lead:'合成红宝石以极低摩擦承托高速旋转的轮轴,同时呈现机芯标志性的深红光点。',
metrics:[['31','宝石总数'],['9 Mohs','硬度'],['0.02','摩擦系数']],
details:[['材料','单晶氧化铝'],['用途','降低轴榫摩擦'],['安装','金套筒压入'],['颜色','铬离子致色']]
}, 7.8));
// Fine screws and banking pins across the movement
[[-4.45,2.55],[4.38,2.7],[-4.2,-3.15],[4.3,-2.8],[-.2,4.42],[2.7,3.75],[-3.1,3.75],[3.75,-1.55]].forEach(([x,y],i)=>{
const s=screw(.12,.1,i%3!==1); s.position.set(x,y,.72); movement.add(s);
});
// Peripheral dial, minute track and luminous applied indices
const dialRing = new THREE.Mesh(new THREE.RingGeometry(5.35,5.86,128),mat.blackMetal);
dialRing.position.z=2.16;
dial.add(dialRing);
const dialEdge=ring(5.58,.08,mat.roseGold,128);dialEdge.position.z=2.23;dial.add(dialEdge);
const minuteRail=ring(5.15,.018,new THREE.MeshBasicMaterial({color:0x80928a,transparent:true,opacity:.45}),128);minuteRail.position.z=2.28;dial.add(minuteRail);
const indexGroup=new THREE.Group();
const lumeMeshes=[];
for(let i=0;i<60;i++){
const a=i/60*Math.PI*2;
const major=i%5===0;
const base=new THREE.Mesh(new THREE.BoxGeometry(major?(i%15===0?.16:.11):.022,major?.58:.16,.09),major?mat.roseGold:mat.brushedSteel);
base.position.set(Math.sin(a)*(major?5.22:5.34),Math.cos(a)*(major?5.22:5.34),2.3);
base.rotation.z=-a;
indexGroup.add(base);
if(major){
const lume=new THREE.Mesh(new THREE.BoxGeometry(i%15===0?.09:.055,.39,.055),mat.lume);
lume.position.set(Math.sin(a)*5.22,Math.cos(a)*5.22,2.37);
lume.rotation.z=-a;
indexGroup.add(lume);lumeMeshes.push(lume);
}
}
dial.add(indexGroup);
// Cardinal markers with doubled geometry at twelve
const twelveL=new THREE.Mesh(new THREE.BoxGeometry(.11,.62,.08),mat.lume);twelveL.position.set(-.13,5.17,2.4);
const twelveR=twelveL.clone();twelveR.material=mat.lume.clone();twelveR.position.x=.13;
dial.add(twelveL,twelveR);lumeMeshes.push(twelveL,twelveR);
// Small seconds register
const secondsRing=ring(1.08,.035,mat.roseGold,72); secondsRing.position.set(2.55,-2.72,2.42); dial.add(secondsRing);
for(let i=0;i<30;i++){
const a=i/30*Math.PI*2;
const tick=new THREE.Mesh(new THREE.BoxGeometry(i%5===0?.035:.018,i%5===0?.18:.10,.02),new THREE.MeshBasicMaterial({color:i%5===0?0xc5a36b:0x66716b}));
tick.position.set(2.55+Math.sin(a)*.92,-2.72+Math.cos(a)*.92,2.44);tick.rotation.z=-a;dial.add(tick);
}
registerPart(dial, {
id:'dial', code:'Reading 08', name:'悬浮式分钟轨道',
lead:'外围分钟圈悬浮于机芯上方,保留完整读时功能,同时不遮挡机械结构。',
metrics:[['60','分钟刻度'],['12','镶贴时标'],['1','小秒盘']],
details:[['底材','黑色钛金属'],['刻度','玫瑰金镶贴'],['分钟轨','铁路式'],['结构','外围悬浮']]
}, 8.8);
function handShape(length,width,tail=.36){
const s=new THREE.Shape();
s.moveTo(-width*.55,-tail);s.lineTo(-width*.9,.1);s.lineTo(-width*.38,length*.78);
s.lineTo(0,length);s.lineTo(width*.38,length*.78);s.lineTo(width*.9,.1);s.lineTo(width*.55,-tail);s.closePath();return s;
}
function createHand(length,width,angle){
const g=new THREE.Group();
const metal=extrudedShape(handShape(length,width),.075,mat.roseGold,.015);
g.add(metal);
const lumeShape=handShape(length*.74,width*.31,.05);
const lume=extrudedShape(lumeShape,.045,mat.lume,.009);
lume.position.y=length*.15;lume.position.z=.065;g.add(lume);lumeMeshes.push(lume);
g.rotation.z=angle;return g;
}
const hands=new THREE.Group();
hands.position.z=2.65;
const hourHand=createHand(2.72,.28,Math.PI/3);
const minuteHand=createHand(4.02,.21,-Math.PI/3);
minuteHand.position.z=.13;
hands.add(hourHand,minuteHand);
const canonPinion=cylinder(.24,.28,mat.roseGold,40);canonPinion.position.z=.18;hands.add(canonPinion);
const cap=cylinder(.11,.33,mat.lume,32);cap.position.z=.34;hands.add(cap);lumeMeshes.push(cap);
const secondsHand=new THREE.Group();secondsHand.position.set(2.55,-2.72,.02);
const secNeedle=new THREE.Mesh(new THREE.BoxGeometry(.027,1.5,.035),mat.lume);secNeedle.position.y=.46;secondsHand.add(secNeedle);lumeMeshes.push(secNeedle);
const secCounter=cylinder(.085,.08,mat.roseGold,24);secondsHand.add(secCounter);hands.add(secondsHand);
display.add(registerPart(hands, {
id:'display', code:'Luminous 09', name:'夜光镂空指针',
lead:'镂空玫瑰金指针填充高强度夜光材料,暗光中呈现悬浮于机芯之上的绿色轮廓。',
metrics:[['10 h','余辉时长'],['1600 K','烧结温度'],['3','显示指针']],
details:[['夜光材料','长余辉铝酸盐'],['指针','玫瑰金镂空'],['读时','小时 · 分钟 · 小秒'],['预设位置','经典 10:10']]
}, 10.4));
// Sapphire crystal: a thin transmitted disc plus polished edge
const sapphire=new THREE.Group();
const crystal=cylinder(5.92,.16,mat.glass,128);crystal.position.z=3.02;sapphire.add(crystal);
const crystalEdge=ring(5.91,.075,mat.glass,144);crystalEdge.position.z=3.04;sapphire.add(crystalEdge);
const glareTex=canvasTexture((ctx,w,h)=>{
const grad=ctx.createLinearGradient(0,0,w,h);
grad.addColorStop(0,'rgba(255,255,255,0)');grad.addColorStop(.42,'rgba(210,255,241,.05)');
grad.addColorStop(.5,'rgba(255,255,255,.2)');grad.addColorStop(.57,'rgba(190,244,228,.03)');grad.addColorStop(1,'rgba(255,255,255,0)');
ctx.fillStyle=grad;ctx.fillRect(0,0,w,h);
},512,512);
const glare=new THREE.Mesh(new THREE.CircleGeometry(5.68,96),new THREE.MeshBasicMaterial({map:glareTex,transparent:true,opacity:.34,depthWrite:false,blending:THREE.AdditiveBlending}));
glare.position.z=3.12;glare.rotation.z=-.32;sapphire.add(glare);
watch.add(registerPart(sapphire, {
id:'sapphire', code:'Crystal 10', name:'拱形蓝宝石水晶',
lead:'高折射率双面防眩蓝宝石形成近乎无形的保护层,并保留边缘冷色反光。',
metrics:[['1.77','折射率'],['9 Mohs','硬度'],['2','防眩镀层']],
details:[['材质','合成蓝宝石'],['轮廓','微拱形'],['镀膜','双面防反射'],['透光率','约 99%']]
}, 13.0));
// Ground reflection, architectural halos and restrained floating dust
const ground=new THREE.Mesh(new THREE.PlaneGeometry(60,60),new THREE.MeshPhysicalMaterial({color:0x050706,metalness:.35,roughness:.34,transparent:true,opacity:.72}));
ground.rotation.x=-Math.PI/2;ground.position.y=-7.4;ground.receiveShadow=true;scene.add(ground);
const haloMat=new THREE.MeshBasicMaterial({color:0x4ebf96,transparent:true,opacity:.045,side:THREE.DoubleSide,blending:THREE.AdditiveBlending,depthWrite:false});
for(let i=0;i<4;i++){
const halo=new THREE.Mesh(new THREE.TorusGeometry(8.7+i*1.55,.012,6,160),haloMat.clone());
halo.position.set(-.8+i*.55,.3-i*.2,-2.4-i*.7);halo.rotation.x=.18;scene.add(halo);
}
const dustGeo=new THREE.BufferGeometry();
const dustCount=isTouch?90:180;const dustPos=new Float32Array(dustCount*3);
for(let i=0;i<dustCount;i++){dustPos[i*3]=(Math.random()-.5)*30;dustPos[i*3+1]=(Math.random()-.5)*22;dustPos[i*3+2]=(Math.random()-.5)*18;}
dustGeo.setAttribute('position',new THREE.BufferAttribute(dustPos,3));
const dust=new THREE.Points(dustGeo,new THREE.PointsMaterial({color:0xb8d6ca,size:.018,transparent:true,opacity:.23,depthWrite:false}));scene.add(dust);
const raycaster=new THREE.Raycaster();
const pointer=new THREE.Vector2(-10,-10);
const tooltip=document.getElementById('tooltip');
const tooltipId=document.getElementById('tooltip-id');
const tooltipName=document.getElementById('tooltip-name');
let hoveredId=null,selectedId=null,pointerDown={x:0,y:0};
function setHighlight(id,on,strength=1){
const entry=parts.get(id);if(!entry)return;
entry.root.traverse(child=>{
if(!child.isMesh)return;
const materials=Array.isArray(child.material)?child.material:[child.material];
materials.forEach(m=>{
if(!m.emissive)return;
const base=m.userData.baseEmissive || new THREE.Color(0);
m.emissive.copy(base).lerp(new THREE.Color(0x5affb8),on?.38*strength:0);
m.emissiveIntensity=on?Math.max(m.userData.baseEmissiveIntensity||1,1.45):m.userData.baseEmissiveIntensity||1;
});
});
}
function updateHover(id,event){
if(hoveredId===id){
if(id)tooltip.style.transform=`translate3d(${event.clientX}px,${event.clientY}px,0)`;
return;
}
if(hoveredId&&hoveredId!==selectedId)setHighlight(hoveredId,false);
hoveredId=id;
if(id){
const data=parts.get(id).data;
setHighlight(id,true,.75);
tooltipId.textContent=data.code;
tooltipName.textContent=data.name;
tooltip.style.transform=`translate3d(${event.clientX}px,${event.clientY}px,0)`;
tooltip.classList.add('visible');
renderer.domElement.style.cursor='pointer';
}else{
tooltip.classList.remove('visible');
renderer.domElement.style.cursor=controls.state===-1?'grab':'grabbing';
}
}
function updatePanel(id){
const panel=document.getElementById('panel');
if(!id){
document.getElementById('panel-kicker').textContent='Manufacture Calibre';
document.getElementById('panel-title').textContent='时间的机械建筑';
document.getElementById('panel-lead').textContent='由 248 枚零件组成的手动上链镂空机芯。拖动观察,悬停识别零件,点击锁定并探索其功能。';
document.getElementById('metric-grid').innerHTML='<div class="metric"><div class="metric-value">72 h</div><div class="metric-label">动力储存</div></div><div class="metric"><div class="metric-value">31</div><div class="metric-label">红宝石</div></div><div class="metric"><div class="metric-value">4 Hz</div><div class="metric-label">摆频</div></div>';
document.getElementById('detail-list').innerHTML='<div class="detail-row"><span>机芯直径</span><span>34.6 mm</span></div><div class="detail-row"><span>结构</span><span>手动上链 · 镂空</span></div><div class="detail-row"><span>表壳</span><span>钛金属 · 蓝宝石水晶</span></div><div class="detail-row"><span>显示</span><span>时 · 分 · 小秒</span></div>';
return;
}
const d=parts.get(id).data;
document.getElementById('panel-kicker').textContent=d.code;
document.getElementById('panel-title').textContent=d.name;
document.getElementById('panel-lead').textContent=d.lead;
document.getElementById('metric-grid').innerHTML=d.metrics.map(m=>`<div class="metric"><div class="metric-value">${m[0]}</div><div class="metric-label">${m[1]}</div></div>`).join('');
document.getElementById('detail-list').innerHTML=d.details.map(row=>`<div class="detail-row"><span>${row[0]}</span><span>${row[1]}</span></div>`).join('');
panel.classList.remove('closed');
}
function selectPart(id){
if(selectedId)setHighlight(selectedId,false);
selectedId=id;
if(id)setHighlight(id,true,1);
updatePanel(id);
}
function hitTest(event){
const rect=renderer.domElement.getBoundingClientRect();
pointer.x=((event.clientX-rect.left)/rect.width)*2-1;
pointer.y=-((event.clientY-rect.top)/rect.height)*2+1;
raycaster.setFromCamera(pointer,camera);
const hit=raycaster.intersectObjects(interactives,false)[0];
return hit?.object.userData.partId || null;
}
renderer.domElement.addEventListener('pointermove',e=>{
if(isTouch)return;
updateHover(hitTest(e),e);
});
renderer.domElement.addEventListener('pointerleave',e=>updateHover(null,e));
renderer.domElement.addEventListener('pointerdown',e=>{pointerDown={x:e.clientX,y:e.clientY};document.getElementById('hint').classList.add('hide');});
renderer.domElement.addEventListener('pointerup',e=>{
const moved=Math.hypot(e.clientX-pointerDown.x,e.clientY-pointerDown.y);
if(moved<5)selectPart(hitTest(e));
});
// Persistent hotspot labels
const hotspotsRoot=document.getElementById('hotspots');
const hotspotElements=new Map();
hotspotIds.forEach(id=>{
const el=document.createElement('button');
el.className='hotspot';el.type='button';el.dataset.id=id;
el.innerHTML=`<span>${parts.get(id).data.name}</span>`;
el.addEventListener('click',()=>selectPart(id));
hotspotsRoot.appendChild(el);hotspotElements.set(id,el);
});
let labelsVisible=false;
function updateHotspots(){
if(!labelsVisible){hotspotsRoot.style.display='none';return;}
hotspotsRoot.style.display='block';
const w=innerWidth,h=innerHeight;
hotspotElements.forEach((el,id)=>{
const obj=parts.get(id).root;
const pos=new THREE.Vector3();obj.getWorldPosition(pos);pos.project(camera);
const visible=pos.z>-1&&pos.z<1&&Math.abs(pos.x)<1.08&&Math.abs(pos.y)<1.08;
el.classList.toggle('offscreen',!visible);
el.style.transform=`translate3d(${(pos.x*.5+.5)*w}px,${(-pos.y*.5+.5)*h}px,0) translate(-4px,-4px)`;
});
}
let explodeTarget=0,explodeProgress=0;
let lumeMode=false;
const explodeButton=document.getElementById('explode-button');
explodeButton.addEventListener('click',()=>{
explodeTarget=explodeTarget?0:1;
explodeButton.classList.toggle('active',!!explodeTarget);
explodeButton.setAttribute('aria-pressed',String(!!explodeTarget));
if(explodeTarget&&!labelsVisible)document.getElementById('label-button').click();
});
const rotateButton=document.getElementById('rotate-button');
rotateButton.addEventListener('click',()=>{
controls.autoRotate=!controls.autoRotate;
rotateButton.classList.toggle('active',controls.autoRotate);
rotateButton.setAttribute('aria-pressed',String(controls.autoRotate));
});
const labelButton=document.getElementById('label-button');
labelButton.addEventListener('click',()=>{
labelsVisible=!labelsVisible;
labelButton.classList.toggle('active',labelsVisible);
labelButton.setAttribute('aria-pressed',String(labelsVisible));
hotspotsRoot.setAttribute('aria-hidden',String(!labelsVisible));
});
document.getElementById('lume-button').addEventListener('click',e=>{
lumeMode=!lumeMode;
e.currentTarget.classList.toggle('active',lumeMode);
e.currentTarget.setAttribute('aria-pressed',String(lumeMode));
renderer.toneMappingExposure=lumeMode?.72:1.16;
bloom.strength=lumeMode?1.24:.52;
bloom.threshold=lumeMode?.42:.72;
mat.lume.emissiveIntensity=lumeMode?5.8:2.6;
lumeMeshes.forEach(mesh=>mesh.material.emissiveIntensity=lumeMode?5.8:2.6);
lights.forEach((light,i)=>light.intensity*=lumeMode?.55:1/.55);
});
let cameraGoal=null,targetGoal=null;
function resetView(){
cameraGoal=new THREE.Vector3(.2,1.1,isTouch?25:22);
targetGoal=new THREE.Vector3(0,0,.8);
selectPart(null);
}
document.getElementById('reset-button').addEventListener('click',resetView);
document.getElementById('focus-button').addEventListener('click',()=>{
if(!selectedId){cameraGoal=new THREE.Vector3(.2,1.1,17);targetGoal=new THREE.Vector3(0,0,1);return;}
const pos=new THREE.Vector3();parts.get(selectedId).root.getWorldPosition(pos);
targetGoal=pos.clone();
const direction=camera.position.clone().sub(controls.target).normalize();
cameraGoal=pos.clone().add(direction.multiplyScalar(9.2));
controls.autoRotate=false;rotateButton.classList.remove('active');rotateButton.setAttribute('aria-pressed','false');
});
const panel=document.getElementById('panel');
document.getElementById('panel-close').addEventListener('click',()=>panel.classList.add('closed'));
document.getElementById('info-toggle').addEventListener('click',()=>panel.classList.toggle('closed'));
let forkAngle=0;
const clock=new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const t=clock.getElapsedTime();
const dt=Math.min(clock.getDelta(),.05);
controls.update();
if(cameraGoal){
camera.position.lerp(cameraGoal,.055);
controls.target.lerp(targetGoal,.065);
if(camera.position.distanceTo(cameraGoal)<.03){cameraGoal=null;targetGoal=null;}
}
explodeProgress=THREE.MathUtils.damp(explodeProgress,explodeTarget,4.2,Math.max(dt,.016));
parts.forEach(({root})=>{
const base=root.userData.basePosition;
if(!base)return;
root.position.z=base.z+(root.userData.explode||0)*explodeProgress;
});
if(!reduceMotion){
animatedGears.forEach((g,i)=>{
if(g.stepped){
const steps=20;g.object.rotation.z=Math.floor(t*g.speed*steps)/steps;
}else g.object.rotation.z+=g.speed*.0026;
});
const beat=Math.sin(t*Math.PI*8);
balanceWheel.rotation.z=beat*.39;
hairspring.scale.setScalar(1+beat*.032);
forkAngle=beat*.12;fork.rotation.z=forkAngle;
secondsHand.rotation.z=-t*Math.PI/30;
crown.rotation.x=Math.sin(t*.22)*.025;
dust.rotation.y=t*.006;dust.position.y=Math.sin(t*.14)*.08;
glare.material.opacity=.24+Math.sin(t*.35)*.08;
}
updateHotspots();
composer.render();
}
controls.addEventListener('start',()=>document.getElementById('hint').classList.add('hide'));
window.addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.setSize(innerWidth,innerHeight);composer.setSize(innerWidth,innerHeight);
bloom.resolution.set(innerWidth,innerHeight);
});
renderer.compile(scene,camera);
animate();
setTimeout(()=>document.getElementById('loading').classList.add('done'),720);
</script>
</body>
</html>