Files
nagi-bench/outputs/glm-5-2/zcode-max/skeleton-watch.html
T
OHCJ099 75b18a76e4 Add skeleton-watch runs: DeepSeek-V4-Pro, Gemini 3.1/3.5, GLM-5.2, Kimi K2.7, MiMo (#23)
Seven skeleton-watch runs across existing models plus the new MiMo Code harness. Validated: validate-data + update-registry --check both pass; artifacts render clean (real Chrome), no external calls beyond standard CDNs, no duplication.
2026-06-21 11:19:25 +01:00

1089 lines
48 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">
<title>SKELETON — 镂空机械腕表 3D 展示</title>
<style>
:root{
--gold:#d9b46a;
--gold-bright:#ffe7a8;
--bg0:#07080b;
--panel:rgba(14,16,22,.72);
--line:rgba(217,180,106,.28);
--txt:#ece7dc;
--muted:#9aa0ab;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg0);color:var(--txt);
font-family:"Helvetica Neue","PingFang SC","Microsoft YaHei",Arial,sans-serif;-webkit-font-smoothing:antialiased}
#app{position:fixed;inset:0}
canvas{display:block}
/* vignette overlay */
#app::after{content:"";position:absolute;inset:0;pointer-events:none;
background:radial-gradient(ellipse at center, transparent 45%, rgba(0,0,0,.55) 100%);}
/* ====== 顶栏品牌 ====== */
.topbar{position:absolute;top:22px;left:0;right:0;display:flex;justify-content:space-between;
align-items:flex-start;padding:0 34px;pointer-events:none;z-index:20}
.brand{pointer-events:auto}
.brand .mark{font-size:11px;letter-spacing:.62em;color:var(--gold);font-weight:600;opacity:.9}
.brand .name{font-size:30px;font-weight:200;letter-spacing:.34em;margin-top:4px;
background:linear-gradient(180deg,#fff,#cbb277);-webkit-background-clip:text;background-clip:text;color:transparent}
.brand .sub{font-size:10px;letter-spacing:.42em;color:var(--muted);margin-top:5px}
.serial{text-align:right;pointer-events:auto}
.serial .l{font-size:9px;letter-spacing:.4em;color:var(--muted)}
.serial .v{font-size:13px;letter-spacing:.24em;color:var(--gold);margin-top:3px}
/* ====== 左控制面板 ====== */
.controls{position:absolute;left:24px;top:50%;transform:translateY(-50%);z-index:20;
width:248px;background:var(--panel);border:1px solid var(--line);border-radius:14px;
padding:20px 18px;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
box-shadow:0 24px 60px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04)}
.controls h3{font-size:10px;letter-spacing:.36em;color:var(--gold);font-weight:600;margin-bottom:16px;
text-transform:uppercase;border-bottom:1px solid var(--line);padding-bottom:12px}
.row{display:flex;align-items:center;justify-content:space-between;margin:13px 0;font-size:12px}
.row .lbl{color:var(--muted);letter-spacing:.06em}
.row .val{color:var(--gold-bright);font-variant-numeric:tabular-nums;font-weight:600}
.btn{display:block;width:100%;padding:11px 12px;margin-top:11px;border-radius:9px;cursor:pointer;
border:1px solid var(--line);background:linear-gradient(180deg,rgba(217,180,106,.16),rgba(217,180,106,.05));
color:var(--txt);font-size:11.5px;letter-spacing:.16em;transition:all .25s ease;font-family:inherit;text-align:left}
.btn:hover{background:linear-gradient(180deg,rgba(217,180,106,.32),rgba(217,180,106,.12));border-color:var(--gold);
transform:translateY(-1px);box-shadow:0 8px 22px rgba(217,180,106,.18)}
.btn.on{background:linear-gradient(180deg,rgba(217,180,106,.45),rgba(217,180,106,.18));color:#1a1207;
font-weight:700;border-color:var(--gold-bright)}
.btn .k{float:right;opacity:.55;font-size:10px;letter-spacing:.1em}
.btn.on .k{opacity:.8;color:#1a1207}
.toggle{position:relative;width:42px;height:22px;border-radius:11px;background:rgba(255,255,255,.08);
cursor:pointer;transition:.25s;border:1px solid var(--line)}
.toggle::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;
background:var(--gold);transition:.25s;box-shadow:0 2px 6px rgba(0,0,0,.5)}
.toggle.on{background:rgba(217,180,106,.4)}
.toggle.on::after{left:22px;background:var(--gold-bright)}
/* ====== 右信息面板 ====== */
.info{position:absolute;right:24px;top:50%;transform:translateY(-50%);z-index:20;width:268px;
background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px 20px;
backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
box-shadow:0 24px 60px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.04);
transition:opacity .4s,transform .4s}
.info .tag{font-size:9px;letter-spacing:.42em;color:var(--gold);font-weight:600}
.info h2{font-size:21px;font-weight:300;letter-spacing:.06em;margin:8px 0 4px}
.info .role{font-size:11px;letter-spacing:.14em;color:var(--muted);margin-bottom:14px}
.info p{font-size:12px;line-height:1.78;color:#cfc9bb;margin-bottom:14px}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px}
.stat{padding:11px 12px;border-radius:9px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.05)}
.stat .k{font-size:9px;letter-spacing:.24em;color:var(--muted)}
.stat .v{font-size:16px;color:var(--gold-bright);font-weight:600;margin-top:5px;font-variant-numeric:tabular-nums}
.info .close{position:absolute;top:12px;right:14px;cursor:pointer;color:var(--muted);font-size:14px;width:22px;height:22px;
display:flex;align-items:center;justify-content:center;border-radius:50%;transition:.2s}
.info .close:hover{color:#fff;background:rgba(255,255,255,.08)}
.info.hidden{opacity:0;transform:translateY(-50%) translateX(20px);pointer-events:none}
/* ====== 底部时间显示 ====== */
.dock{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);z-index:20;text-align:center}
.dock .clock{font-size:38px;font-weight:200;letter-spacing:.14em;color:var(--gold-bright);
font-variant-numeric:tabular-nums;text-shadow:0 0 24px rgba(217,180,106,.35)}
.dock .clock .sep{opacity:.5;animation:blink 1s infinite}
@keyframes blink{50%{opacity:.1}}
.dock .hint{font-size:10px;letter-spacing:.34em;color:var(--muted);margin-top:7px}
/* ====== 悬浮零件标签 ====== */
.tag3d{position:absolute;z-index:25;pointer-events:none;transform:translate(-50%,-50%);
transition:opacity .25s ease;opacity:0}
.tag3d .dot{width:9px;height:9px;border-radius:50%;background:var(--gold-bright);
box-shadow:0 0 12px var(--gold),0 0 0 3px rgba(217,180,106,.18);margin:0 auto}
.tag3d .line{width:1px;height:26px;background:linear-gradient(180deg,var(--gold-bright),transparent);margin:0 auto}
.tag3d .lbl{white-space:nowrap;font-size:10.5px;letter-spacing:.14em;color:var(--gold-bright);
background:rgba(8,9,13,.78);border:1px solid var(--line);padding:4px 9px;border-radius:5px;margin-top:2px}
.tag3d.show{opacity:1}
/* hint for hover in canvas */
.cursor-pointer{cursor:pointer}
/* loader */
#loader{position:fixed;inset:0;background:var(--bg0);z-index:100;display:flex;align-items:center;justify-content:center;
flex-direction:column;transition:opacity .8s}
#loader .sp{width:46px;height:46px;border-radius:50%;border:2px solid rgba(217,180,106,.18);
border-top-color:var(--gold);animation:spin 1s linear infinite}
#loader .t{margin-top:20px;font-size:11px;letter-spacing:.4em;color:var(--muted)}
@keyframes spin{to{transform:rotate(360deg)}}
@media (max-width:880px){
.controls,.info{width:calc(100% - 48px);position:absolute;transform:none;top:auto}
.controls{top:90px}.info{bottom:90px;top:auto;transform:none}
.info.hidden{transform:translateY(20px)}
.topbar{padding:0 18px}.brand .name{font-size:22px}
.dock{display:none}
}
</style>
</head>
<body>
<div id="app"></div>
<div class="topbar">
<div class="brand">
<div class="mark">MANUFACTURE · GENÈVE</div>
<div class="name">SKELETON</div>
<div class="sub">CAL. T-9128 · TOURBILLON STYLE</div>
</div>
<div class="serial">
<div class="l">RÉFÉRENCE</div>
<div class="v">N°0247 / 250</div>
</div>
</div>
<div class="controls" id="controls">
<h3>CONTRÔLES</h3>
<div class="row"><span class="lbl">自动旋转</span><div class="toggle on" id="tAuto"></div></div>
<div class="row"><span class="lbl">机芯运转</span><div class="toggle on" id="tRun"></div></div>
<div class="row"><span class="lbl">背景反射</span><div class="toggle on" id="tRefl"></div></div>
<div class="row"><span class="lbl">辉光 (Bloom)</span><div class="toggle on" id="tBloom"></div></div>
<div class="row"><span class="lbl">摆轮速率</span><span class="val" id="rateVal">5.0 Hz</span></div>
<button class="btn" id="bExplode">爆炸视图<span class="k">EXPLODE</span></button>
<button class="btn" id="bReset">重置视角<span class="k">RESET</span></button>
<div style="margin-top:18px;font-size:10px;color:var(--muted);line-height:1.9;letter-spacing:.04em">
鼠标拖拽旋转 · 滚轮缩放<br>
悬停零件查看名称 · 点击展开详情
</div>
</div>
<div class="info hidden" id="info">
<div class="close" id="closeInfo">✕</div>
<div class="tag" id="iTag">COMPOSANT</div>
<h2 id="iTitle">零件</h2>
<div class="role" id="iRole">功能</div>
<p id="iDesc">描述</p>
<div class="stats" id="iStats"></div>
</div>
<div class="dock">
<div class="clock"><span id="hh">00</span><span class="sep">:</span><span id="mm">00</span><span class="sep">:</span><span id="ss">00</span></div>
<div class="hint">SYNCHRONIZED · LOCAL TIME</div>
</div>
<div id="loader"><div class="sp"></div><div class="t">CALIBRATING MOVEMENT…</div></div>
<script type="importmap">
{
"imports":{
"three":"https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<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';
/* ============================================================
全局状态
============================================================ */
const state = {
autoRotate:true, movement:true, reflection:true, bloom:true,
rate:5.0, exploded:false, explodeT:0
};
const clock = new THREE.Clock();
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2(-2,-2);
/* ============================================================
场景 / 相机 / 渲染器
============================================================ */
const app = document.getElementById('app');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x07080b);
scene.fog = new THREE.Fog(0x07080b, 22, 60);
const camera = new THREE.PerspectiveCamera(34, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0, 6.5, 13);
const renderer = new THREE.WebGLRenderer({antialias:true, alpha:false});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
renderer.outputColorSpace = THREE.SRGBColorSpace;
app.appendChild(renderer.domElement);
/* ============================================================
环境贴图(程序化生成,营造高端摄影棚反射)
============================================================ */
function makeEnvTexture(){
const c = document.createElement('canvas'); c.width=512; c.height=256;
const g = c.getContext('2d');
// base dark
g.fillStyle='#0a0c12'; g.fillRect(0,0,512,256);
// gradient studio
const grd=g.createLinearGradient(0,0,0,256);
grd.addColorStop(0,'#1a1f2b'); grd.addColorStop(0.5,'#0a0c12'); grd.addColorStop(1,'#05060a');
g.fillStyle=grd; g.fillRect(0,0,512,256);
// key light strip (warm)
const k=g.createRadialGradient(150,90,5,150,90,140);
k.addColorStop(0,'rgba(255,220,160,0.95)'); k.addColorStop(0.3,'rgba(255,200,130,0.45)');
k.addColorStop(1,'rgba(0,0,0,0)');
g.fillStyle=k; g.fillRect(0,0,512,256);
// fill strip (cool)
const f=g.createRadialGradient(400,120,5,400,120,120);
f.addColorStop(0,'rgba(140,170,255,0.55)'); f.addColorStop(1,'rgba(0,0,0,0)');
g.fillStyle=f; g.fillRect(0,0,512,256);
// rim
const r=g.createRadialGradient(256,20,5,256,20,90);
r.addColorStop(0,'rgba(255,255,255,0.7)'); r.addColorStop(1,'rgba(0,0,0,0)');
g.fillStyle=r; g.fillRect(0,0,512,256);
// bottom subtle bounce
const b=g.createLinearGradient(0,200,0,256);
b.addColorStop(0,'rgba(0,0,0,0)'); b.addColorStop(1,'rgba(120,90,40,0.35)');
g.fillStyle=b; g.fillRect(0,0,512,256);
const tex = new THREE.CanvasTexture(c);
tex.mapping = THREE.EquirectangularReflectionMapping;
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
const envTex = makeEnvTexture();
const pmrem = new THREE.PMREMGenerator(renderer);
const envMap = pmrem.fromEquirectangular(envTex).texture;
scene.environment = envMap;
/* ============================================================
光照(三点 + 边缘)
============================================================ */
const ambient = new THREE.AmbientLight(0x6a7080, 0.35);
scene.add(ambient);
const keyLight = new THREE.DirectionalLight(0xfff0d8, 2.6);
keyLight.position.set(7,12,8);
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048,2048);
keyLight.shadow.camera.near=1; keyLight.shadow.camera.far=40;
keyLight.shadow.camera.left=-12; keyLight.shadow.camera.right=12;
keyLight.shadow.camera.top=12; keyLight.shadow.camera.bottom=-12;
keyLight.shadow.bias=-0.0003; keyLight.shadow.radius=4;
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0x88aaff, 0.9);
fillLight.position.set(-9,4,-3);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xffd9a0, 1.6);
rimLight.position.set(0,7,-10);
scene.add(rimLight);
const accentPoint = new THREE.PointLight(0xffe7b0, 1.2, 22, 2);
accentPoint.position.set(0,3,4);
scene.add(accentPoint);
/* ============================================================
反射地板
============================================================ */
const floorMat = new THREE.MeshStandardMaterial({
color:0x0a0b10, metalness:0.9, roughness:0.35, envMap:envMap, envMapIntensity:0.5
});
const floor = new THREE.Mesh(new THREE.CircleGeometry(30,64), floorMat);
floor.rotation.x = -Math.PI/2; floor.position.y = -3.2;
floor.receiveShadow = true;
scene.add(floor);
/* ============================================================
控制器
============================================================ */
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.06;
controls.minDistance = 6; controls.maxDistance = 22;
controls.maxPolarAngle = Math.PI*0.52; controls.minPolarAngle = Math.PI*0.12;
controls.target.set(0,0,0);
controls.autoRotate = state.autoRotate;
controls.autoRotateSpeed = 0.6;
/* ============================================================
材质库
============================================================ */
const M = {
gold: new THREE.MeshPhysicalMaterial({color:0xd9b46a, metalness:1, roughness:0.18, envMap:envMap, envMapIntensity:1.4, clearcoat:0.6, clearcoatRoughness:0.15}),
goldPolish:new THREE.MeshPhysicalMaterial({color:0xf4d68a, metalness:1, roughness:0.09, envMap:envMap, envMapIntensity:1.6, clearcoat:0.9, clearcoatRoughness:0.08}),
steel: new THREE.MeshPhysicalMaterial({color:0xb8bcc4, metalness:1, roughness:0.28, envMap:envMap, envMapIntensity:1.2}),
steelDark:new THREE.MeshPhysicalMaterial({color:0x4a4e58, metalness:1, roughness:0.35, envMap:envMap, envMapIntensity:1.0}),
brass: new THREE.MeshPhysicalMaterial({color:0xc9a45a, metalness:1, roughness:0.22, envMap:envMap, envMapIntensity:1.4}),
bluedSteel:new THREE.MeshPhysicalMaterial({color:0x2a4cc8, metalness:1, roughness:0.18, envMap:envMap, envMapIntensity:1.5, clearcoat:0.7}),
ruby: new THREE.MeshPhysicalMaterial({color:0xff2a4d, metalness:0.1, roughness:0.05, envMap:envMap, envMapIntensity:1.8, transmission:0.55, thickness:0.4, ior:1.77, emissive:0x550010, emissiveIntensity:0.4, clearcoat:1}),
sapphire:new THREE.MeshPhysicalMaterial({color:0xeaf2ff, metalness:0, roughness:0.02, envMap:envMap, envMapIntensity:1.5, transmission:0.96, thickness:0.5, ior:1.77, clearcoat:1, clearcoatRoughness:0.0, transparent:true, opacity:0.35}),
lume: new THREE.MeshStandardMaterial({color:0xb4ff8a, emissive:0x7dff5a, emissiveIntensity:1.4, roughness:0.4, metalness:0}),
hairlinePlate:new THREE.MeshPhysicalMaterial({color:0xc8b070, metalness:1, roughness:0.32, envMap:envMap, envMapIntensity:1.2, clearcoat:0.3}),
blackDial:new THREE.MeshPhysicalMaterial({color:0x0c0e14, metalness:0.6, roughness:0.45, envMap:envMap, envMapIntensity:0.6}),
darkMetal:new THREE.MeshStandardMaterial({color:0x16181f, metalness:0.9, roughness:0.5, envMap:envMap, envMapIntensity:0.7})
};
/* ============================================================
工具:齿轮几何体(带渐开线近似齿)
============================================================ */
function makeGearGeometry(teeth, outerR, innerR, holeR, depth, bevel=0.06){
const shape = new THREE.Shape();
const step = (Math.PI*2)/teeth;
const toothW = step*0.46;
const rootR = outerR - bevel*2;
for(let i=0;i<teeth;i++){
const a = i*step;
const a1 = a, a2 = a + (step-toothW)/2, a3 = a + (step-toothW)/2 + toothW*0.18,
a4 = a + step - toothW*0.18, a5 = a + step;
if(i===0) shape.moveTo(Math.cos(a1)*rootR, Math.sin(a1)*rootR);
shape.lineTo(Math.cos(a2)*rootR, Math.sin(a2)*rootR);
shape.lineTo(Math.cos(a3)*outerR, Math.sin(a3)*outerR);
shape.lineTo(Math.cos(a4)*outerR, Math.sin(a4)*outerR);
shape.lineTo(Math.cos(a5)*rootR, Math.sin(a5)*rootR);
}
// 中心孔
const hole = new THREE.Path();
hole.absarc(0,0,holeR,0,Math.PI*2,true);
shape.holes.push(hole);
// 减重孔(轻量化镂空)
const spokes = 5;
for(let s=0;s<spokes;s++){
const ang = (s/spokes)*Math.PI*2;
const cx = Math.cos(ang)*((innerR+holeR)/1.7);
const cy = Math.sin(ang)*((innerR+holeR)/1.7);
const r = (innerR-holeR)*0.32;
const p = new THREE.Path();
p.absarc(cx,cy,r,0,Math.PI*2,true);
shape.holes.push(p);
}
const geo = new THREE.ExtrudeGeometry(shape,{depth,bevelEnabled:true,bevelThickness:bevel,bevelSize:bevel,bevelSegments:2,steps:1});
geo.translate(0,0,-depth/2);
geo.computeVertexNormals();
return geo;
}
/* 中心带盖的齿轮(发条盒等) */
function makeBarrelGeometry(teeth, outerR, holeR, depth, bevel=0.05){
const shape = new THREE.Shape();
const step=(Math.PI*2)/teeth, toothW=step*0.46, rootR=outerR-bevel*2;
for(let i=0;i<teeth;i++){
const a=i*step;
if(i===0) shape.moveTo(Math.cos(a)*rootR, Math.sin(a)*rootR);
shape.lineTo(Math.cos(a+(step-toothW)/2)*rootR, Math.sin(a+(step-toothW)/2)*rootR);
shape.lineTo(Math.cos(a+(step-toothW)/2+toothW*0.18)*outerR, Math.sin(a+(step-toothW)/2+toothW*0.18)*outerR);
shape.lineTo(Math.cos(a+step-toothW*0.18)*outerR, Math.sin(a+step-toothW*0.18)*outerR);
shape.lineTo(Math.cos(a+step)*rootR, Math.sin(a+step)*rootR);
}
const hole=new THREE.Path(); hole.absarc(0,0,holeR,0,Math.PI*2,true); shape.holes.push(hole);
const geo=new THREE.ExtrudeGeometry(shape,{depth,bevelEnabled:true,bevelThickness:bevel,bevelSize:bevel,bevelSegments:2,steps:1});
geo.translate(0,0,-depth/2); geo.computeVertexNormals();
return geo;
}
/* ============================================================
标记零件组(用于交互)
============================================================ */
const interactive = [];
let hoverGroup = null;
function markPart(group, info){
group.userData.info = info;
group.traverse(o=>{ if(o.isMesh){o.userData.parentGroup=group;} });
interactive.push(group);
return group;
}
/* 用于高亮:为每个被高亮网格克隆材质,避免共享材质互相影响 */
const _origMat = new WeakMap();
function setHighlight(group, on){
group.traverse(o=>{
if(!o.isMesh) return;
if(on){
if(!_origMat.has(o)){
const m = o.material;
_origMat.set(o, m);
const c = m.clone();
if(c.emissive){ c.emissive.setHex(0xffcf7a); c.emissiveIntensity = 0.85; }
else { // 标准材质若无 emissive 属性则补一个发光
c.emissive = new THREE.Color(0xffcf7a); c.emissiveIntensity = 0.8;
}
o.material = c;
}
} else {
if(_origMat.has(o)){ o.material = _origMat.get(o); _origMat.delete(o); }
}
});
}
/* ============================================================
构建手表主组
============================================================ */
const watch = new THREE.Group();
watch.rotation.x = 0.42; // 向相机倾斜,呈现 3/4 英雄视角
scene.add(watch);
const movement = new THREE.Group(); // 机芯组
watch.add(movement);
const caseParts = new THREE.Group(); // 外壳组
watch.add(caseParts);
/* ----- 表壳 / 表圈 ----- */
function buildCase(){
// 主夹板基座(圆形底)
const baseR = 5.0;
const base = new THREE.Mesh(new THREE.CylinderGeometry(baseR, baseR, 0.18, 80), M.hairlinePlate);
base.position.y = -0.45; base.castShadow=true; base.receiveShadow=true;
caseParts.add(base);
// 表壳中框(金属环)
const bezel = new THREE.Mesh(new THREE.CylinderGeometry(5.2,5.2,0.55,90,1,true), M.gold);
bezel.position.y = -0.1; bezel.castShadow=true; bezel.receiveShadow=true;
caseParts.add(bezel);
// 表圈顶部(抛光金圈)
const ringTop = new THREE.Mesh(new THREE.TorusGeometry(5.05,0.16,18,90), M.goldPolish);
ringTop.rotation.x = Math.PI/2; ringTop.position.y = 0.18;
ringTop.castShadow=true;
caseParts.add(ringTop);
// 表圈底部
const ringBot = new THREE.Mesh(new THREE.TorusGeometry(5.05,0.16,18,90), M.goldPolish);
ringBot.rotation.x = Math.PI/2; ringBot.position.y = -0.5;
caseParts.add(ringBot);
// 蓝宝石水晶玻璃(顶面)
const glass = new THREE.Mesh(new THREE.CylinderGeometry(4.9,4.9,0.12,80), M.sapphire);
glass.position.y = 0.22;
caseParts.add(glass);
// 玻璃上沿倒角圈
const glassBev = new THREE.Mesh(new THREE.TorusGeometry(4.88,0.05,12,80), M.goldPolish);
glassBev.rotation.x=Math.PI/2; glassBev.position.y=0.28; caseParts.add(glassBev);
// 底盖
const back = new THREE.Mesh(new THREE.CylinderGeometry(5.0,4.95,0.2,80), M.goldPolish);
back.position.y = -0.62; back.castShadow=true;
caseParts.add(back);
// 表耳(lugs)4个
for(let i=0;i<4;i++){
const ang = (i<2? (i===0?0.42:-0.42):(i===2?Math.PI-0.42:Math.PI+0.42));
const lug = new THREE.Mesh(new THREE.BoxGeometry(0.55,0.7,0.9), M.gold);
lug.position.set(Math.cos(ang)*4.9, -0.1, Math.sin(ang)*4.9);
lug.rotation.y = -ang + Math.PI/2;
lug.castShadow=true;
caseParts.add(lug);
}
// 表冠
const crown = new THREE.Group();
const cBody = new THREE.Mesh(new THREE.CylinderGeometry(0.32,0.32,0.45,24), M.goldPolish);
cBody.rotation.z = Math.PI/2;
const cTip = new THREE.Mesh(new THREE.SphereGeometry(0.32,24,16), M.goldPolish);
cTip.position.x = 0.22;
// 齿纹(小柱)
for(let i=0;i<16;i++){
const a=i/16*Math.PI*2;
const t = new THREE.Mesh(new THREE.BoxGeometry(0.4,0.05,0.05), M.goldPolish);
t.position.set(0,Math.cos(a)*0.32,Math.sin(a)*0.32); t.rotation.x=a;
crown.add(t);
}
crown.add(cBody); crown.add(cTip);
crown.position.set(5.55,-0.1,0);
crown.castShadow=true;
caseParts.add(crown);
// 外环刻度(表盘外圈分钟刻度)
const scaleRing = new THREE.Group();
for(let i=0;i<60;i++){
const a = Math.PI/2 - i/60*Math.PI*2; // 12 点位于 +Z,顺时针
const major = i%5===0;
const tickGeo = new THREE.BoxGeometry(major?0.07:0.035, 0.02, major?0.34:0.18);
const tickMat = major ? M.lume.clone() : new THREE.MeshStandardMaterial({color:0xcfcab8, metalness:0.6, roughness:0.3, envMap:envMap, envMapIntensity:1});
const tick = new THREE.Mesh(tickGeo, tickMat);
tick.position.set(Math.cos(a)*4.55, 0.05, Math.sin(a)*4.55);
tick.rotation.y = -a + Math.PI/2;
scaleRing.add(tick);
}
// 数字时标(荧光,仅12个位置贴块)
for(let i=0;i<12;i++){
const a = Math.PI/2 - i/12*Math.PI*2; // 12 点位于 +Z,顺时针
const m = new THREE.Mesh(new THREE.BoxGeometry(0.12,0.03,0.18), M.lume);
m.position.set(Math.cos(a)*4.35, 0.06, Math.sin(a)*4.35);
m.rotation.y = -a + Math.PI/2;
scaleRing.add(m);
}
caseParts.add(scaleRing);
// 表带片段(装饰,上下各两节)
function strap(off, flip){
const grp = new THREE.Group();
for(let i=0;i<3;i++){
const seg = new THREE.Mesh(new THREE.BoxGeometry(1.4,0.32,0.7), M.gold);
seg.position.z = i*0.62 + 0.5;
seg.castShadow=true;
grp.add(seg);
const link = new THREE.Mesh(new THREE.CylinderGeometry(0.12,0.12,1.5,16), M.steel);
link.rotation.z = Math.PI/2; link.position.z = i*0.62 + 0.85;
grp.add(link);
}
grp.position.set(off?3.2:-3.2,-0.1,0); grp.rotation.y = off?0:Math.PI;
return grp;
}
caseParts.add(strap(false)); caseParts.add(strap(true));
}
buildCase();
/* ============================================================
机芯构造
============================================================ */
/* --- 主夹板(带日内瓦波纹 + 红宝石轴承 + 螺丝) --- */
function buildMainPlate(){
const plateGroup = new THREE.Group();
// 主夹板(圆形,带凹陷镂空造型)
const plateShape = new THREE.Shape();
plateShape.absarc(0,0,4.6,0,Math.PI*2,false);
// 镂空窗孔
const windows = [[0,0,1.3],[2.6,1.6,0.9],[-2.6,1.6,0.9],[2.6,-1.6,0.9],[-2.6,-1.6,0.9],[0,2.6,0.7]];
windows.forEach(w=>{
const p=new THREE.Path(); p.absarc(w[0],w[1],w[2],0,Math.PI*2,true); plateShape.holes.push(p);
});
const plateGeo = new THREE.ExtrudeGeometry(plateShape,{depth:0.12,bevelEnabled:true,bevelThickness:0.03,bevelSize:0.03,bevelSegments:2,steps:1});
plateGeo.translate(0,0,-0.06);
const plate = new THREE.Mesh(plateGeo, M.hairlinePlate);
plate.rotation.x = -Math.PI/2; // 转为水平夹板(法线朝 +Y)
plate.position.y = -0.3;
plate.castShadow=true; plate.receiveShadow=true;
plateGroup.add(plate);
// 日内瓦波纹(程序化贴图覆盖在板面上)
const waveC = document.createElement('canvas'); waveC.width=waveC.height=512;
const wg = waveC.getContext('2d');
wg.fillStyle='#1a160c'; wg.fillRect(0,0,512,512);
for(let r=8;r<400;r+=8){
wg.strokeStyle = `rgba(255,220,150,${r%16===0?0.22:0.10})`;
wg.lineWidth=2;
wg.beginPath(); wg.arc(256,256,r,0,Math.PI*2); wg.stroke();
}
const waveTex = new THREE.CanvasTexture(waveC); waveTex.colorSpace=THREE.SRGBColorSpace;
const waveMat = new THREE.MeshPhysicalMaterial({map:waveTex, color:0xc9a45a, metalness:1, roughness:0.35, envMap:envMap, envMapIntensity:1.1, clearcoat:0.4});
const wave = new THREE.Mesh(new THREE.CircleGeometry(4.5,64), waveMat);
wave.rotation.x=-Math.PI/2; wave.position.y=-0.22;
wave.receiveShadow=true;
plateGroup.add(wave);
// 红宝石轴承(与各齿轮轴心对齐)
const jewelPos = [[0,0],[1.85,1.05],[2.15,-0.95],[0.85,-1.85],[-1.7,-1.75],[-2.6,0.6]];
jewelPos.forEach(p=>{
// 金质宝石座
const seat = new THREE.Mesh(new THREE.CylinderGeometry(0.16,0.18,0.1,18), M.goldPolish);
seat.position.set(p[0],-0.14,p[1]); plateGroup.add(seat);
// 红宝石
const jewel = new THREE.Mesh(new THREE.SphereGeometry(0.1,18,12), M.ruby);
jewel.position.set(p[0],-0.08,p[1]); jewel.scale.y=0.5;
plateGroup.add(jewel);
});
// 蓝钢螺丝(固定夹板用,沿板边缘分布)
const screwPos = [[1.5,-1.4],[-1.5,-1.4],[1.5,1.4],[-1.5,1.4],[3.4,0.6],[-3.4,-0.6]];
screwPos.forEach(p=>{
const head = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.06,12), M.bluedSteel);
head.position.set(p[0],-0.1,p[1]);
plateGroup.add(head);
// 一字槽
const slot = new THREE.Mesh(new THREE.BoxGeometry(0.16,0.02,0.03), M.steelDark);
slot.position.set(p[0],-0.07,p[1]); slot.rotation.y = Math.random()*Math.PI;
plateGroup.add(slot);
});
movement.add(plateGroup);
markPart(plateGroup,{
tag:'MAIN PLATE', title:'主夹板', role:'机芯基础结构 / Mainplate',
desc:'机芯的基座结构,承载所有齿轮、擒纵与摆轮组件。表面饰有日内瓦波纹(Côtes de Genève),体现顶级制表的装饰打磨工艺。',
stats:[['直径','36 mm'],['材质','镀铑黄铜',''],['波纹','日内瓦','']]
});
return plateGroup;
}
const plateGroup = buildMainPlate();
/* --- 桥板(夹板,悬于齿轮上方) --- */
function buildBridges(){
const bg = new THREE.Group();
// 主桥板1(横跨齿轮系:覆盖中心轮/三轮/四轮区域)
// 形状定义在 XY 平面;rotateX(-PI/2) 后 shape.Y -> -world.Z
const bShape = new THREE.Shape();
bShape.moveTo(-2.3,-1.2);
bShape.bezierCurveTo(-2.3,1.0,-1.3,1.25,0,1.25);
bShape.bezierCurveTo(1.3,1.25,2.3,1.0,2.3,-1.2);
bShape.bezierCurveTo(1.4,-1.05,0.7,-0.85,0,-0.85);
bShape.bezierCurveTo(-0.7,-0.85,-1.4,-1.05,-2.3,-1.2);
const bGeo = new THREE.ExtrudeGeometry(bShape,{depth:0.22,bevelEnabled:true,bevelThickness:0.04,bevelSize:0.04,bevelSegments:2,steps:1});
bGeo.rotateX(-Math.PI/2); bGeo.translate(0,0,0);
const bridge1 = new THREE.Mesh(bGeo, M.goldPolish);
bridge1.position.y = -0.02; bridge1.castShadow=true; bridge1.receiveShadow=true;
bg.add(bridge1);
// 摆轮夹板(独立弧形,覆盖摆轮位置)
const pShape = new THREE.Shape();
pShape.absarc(0,0,1.05,0.3,Math.PI-0.3,false);
pShape.absarc(0,0,0.75,Math.PI-0.3,0.3,true);
const pGeo = new THREE.ExtrudeGeometry(pShape,{depth:0.2,bevelEnabled:true,bevelThickness:0.03,bevelSize:0.03,bevelSegments:2,steps:1});
pGeo.rotateX(-Math.PI/2); pGeo.translate(-1.7,0,-1.55);
const bridge2 = new THREE.Mesh(pGeo, M.goldPolish);
bridge2.castShadow=true;
bg.add(bridge2);
// 桥板上的螺丝(位于桥板上方)
[[1.7,0.8],[-1.7,0.8],[0,1.2],[-1.7,-2.2]].forEach(p=>{
const sc = new THREE.Mesh(new THREE.CylinderGeometry(0.09,0.09,0.06,12), M.bluedSteel);
sc.position.set(p[0],0.22,p[1]); bg.add(sc);
});
// 桥板上的红宝石轴承(齿轮上支点)
[[0,0],[-1.7,-1.75]].forEach(p=>{
const j=new THREE.Mesh(new THREE.SphereGeometry(0.08,16,10), M.ruby);
j.position.set(p[0],0.24,p[1]); j.scale.y=0.5; bg.add(j);
});
// 倒角打磨条纹(鱼鳞纹模拟:贴图小圆点)—— 用若干小球点缀
for(let i=0;i<16;i++){
const dot = new THREE.Mesh(new THREE.SphereGeometry(0.04,8,6), M.gold);
const ang = Math.random()*Math.PI*2;
const rr = 0.3 + Math.random()*1.8;
dot.position.set(Math.cos(ang)*rr*(Math.random()<0.5?1:0.6),0.21,0.4+Math.sin(ang)*rr*0.5);
bg.add(dot);
}
movement.add(bg);
markPart(bg,{
tag:'BRIDGES', title:'夹板 / 桥板', role:'支撑与定位 / Bridges',
desc:'覆盖在主夹板之上,固定齿轮轴心并提供精确轴承定位。表面经过 anglage(倒角)与 perlage(鱼鳞纹)打磨,边缘抛光至镜面,是高级机芯的视觉标志。',
stats:[['数量','2 件'],['打磨','鱼鳞纹 + 倒角',''],['螺丝','蓝钢','']]
});
return bg;
}
const bridgeGroup = buildBridges();
/* --- 发条盒(主发条) --- */
function buildMainspring(){
const g = new THREE.Group();
const r = 1.45;
// 外齿圈
const outer = new THREE.Mesh(makeBarrelGeometry(64, r, 0.2, 0.45), M.goldPolish);
outer.rotation.x = -Math.PI/2;
outer.position.y = -0.02;
outer.castShadow=true;
g.add(outer);
// 发条盒盖(顶面,带同心圆发条纹理)
const lidC = document.createElement('canvas'); lidC.width=lidC.height=256;
const lg = lidC.getContext('2d');
lg.fillStyle='#caa45c'; lg.fillRect(0,0,256,256);
lg.strokeStyle='rgba(80,55,20,0.7)'; lg.lineWidth=2;
for(let i=4;i<120;i+=3){ lg.beginPath(); lg.arc(128,128,i,0,Math.PI*2); lg.stroke(); }
const lidTex = new THREE.CanvasTexture(lidC); lidTex.colorSpace=THREE.SRGBColorSpace;
const lid = new THREE.Mesh(new THREE.CircleGeometry(r-0.1,64), new THREE.MeshPhysicalMaterial({map:lidTex, metalness:1, roughness:0.3, envMap:envMap, envMapIntensity:1.2}));
lid.rotation.x=-Math.PI/2; lid.position.y=0.21;
g.add(lid);
// 中心轴
const arbor = new THREE.Mesh(new THREE.CylinderGeometry(0.18,0.18,0.6,20), M.steel);
arbor.position.y=0.05; g.add(arbor);
const arborTop = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.2,16), M.bluedSteel);
arborTop.position.y=0.4; g.add(arborTop);
g.position.set(-2.6,0,0.6);
movement.add(g);
markPart(g,{
tag:'MAINSPRING BARREL', title:'发条盒', role:'动力源 / Power Reserve 72h',
desc:'主发条缠绕于盒内,是腕表的核心动力源。手动上链时表冠旋转卷紧发条,缓慢释放的能量驱动整个齿轮系运转,可提供长达 72 小时的动力储备。',
stats:[['齿数','64',''],['动力储备','72 小时',''],['上链方式','手动','']]
});
return {g, arbor};
}
const mainspring = buildMainspring();
/* --- 齿轮系(gear train) --- */
function buildGearTrain(){
const gt = new THREE.Group();
const wheels = [];
// 配置:位置(x,z) 齿数 半径 深 颜色 转速系数
const cfg = [
{pos:[0,0], teeth:60, r:1.0, d:0.18, mat:M.goldPolish, w:1.0, name:'中心轮', tag:'CENTER WHEEL'},
{pos:[1.85,1.05], teeth:48, r:0.78, d:0.16, mat:M.gold, w:4.0, name:'第三轮', tag:'THIRD WHEEL'},
{pos:[2.15,-0.95], teeth:42, r:0.66, d:0.15, mat:M.gold, w:16.0, name:'第四轮', tag:'FOURTH WHEEL'},
{pos:[0.85,-1.85], teeth:24, r:0.42, d:0.14, mat:M.brass, w:80.0, name:'擒纵轮', tag:'ESCAPE WHEEL'},
];
cfg.forEach(c=>{
const w = new THREE.Group();
const gear = new THREE.Mesh(makeGearGeometry(c.teeth, c.r, c.r*0.65, 0.12, c.d), c.mat);
gear.rotation.x = -Math.PI/2;
gear.position.y = 0.05;
gear.castShadow=true;
w.add(gear);
// 轴心
const pin = new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.08,0.4,12), M.steel);
pin.position.y=0.15; w.add(pin);
// 擒纵轮特有尖齿造型已由 teeth 与几何参数体现
w.position.set(c.pos[0],0,c.pos[1]);
w.userData.spin = c.w; // 相对转速
w.userData.cfg = c;
gt.add(w);
wheels.push(w);
markPart(w,{
tag:c.tag, title:c.name, role:'齿轮系 / Wheel Train',
desc:`${c.name} — 齿轮系的关键一环,将发条能量逐级传递并放大转速。齿轮经过镜面抛光,齿缘倒角处理,配合红宝石轴承减少摩擦。`,
stats:[['齿数',String(c.teeth),''],['直径',(c.r*36).toFixed(1)+' mm',''],['功能','传动','']]
});
});
movement.add(gt);
return {gt,wheels};
}
const {gt:gearTrain, wheels} = buildGearTrain();
/* --- 擒纵机构(擒纵叉) --- */
function buildEscapement(){
const g = new THREE.Group();
const fork = new THREE.Group();
// 叉身
const body = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.08,0.1), M.steel);
fork.add(body);
// 叉头(宝石)
const horn1 = new THREE.Mesh(new THREE.BoxGeometry(0.1,0.06,0.1), M.steel);
horn1.position.set(0.25,0,0.1); fork.add(horn1);
const horn2 = new THREE.Mesh(new THREE.BoxGeometry(0.1,0.06,0.1), M.steel);
horn2.position.set(0.25,0,-0.1); fork.add(horn2);
// 叉瓦(红宝石)
const pallet1 = new THREE.Mesh(new THREE.BoxGeometry(0.08,0.08,0.05), M.ruby);
pallet1.position.set(-0.22,0,0.07); fork.add(pallet1);
const pallet2 = new THREE.Mesh(new THREE.BoxGeometry(0.08,0.08,0.05), M.ruby);
pallet2.position.set(-0.22,0,-0.07); fork.add(pallet2);
// 中心枢轴
const pivot = new THREE.Mesh(new THREE.CylinderGeometry(0.06,0.06,0.25,12), M.bluedSteel);
pivot.position.set(-0.4,0.1,0); pivot.rotation.z=0.2; fork.add(pivot);
fork.position.set(0.55,0.08,-1.25);
fork.rotation.y = 1.15; // 指向擒纵轮方向
fork.userData.isFork = true;
fork.userData.baseY = 1.15;
g.add(fork);
movement.add(g);
markPart(fork,{
tag:'PALLET FORK', title:'擒纵叉', role:'能量调节 / Swiss Lever',
desc:'瑞士叉瓦式擒纵机构的核心。擒纵叉在擒纵轮与摆轮之间往复运动,叉瓦上的两颗红宝石(进瓦与出瓦)精确控制能量释放,将旋转运动转化为摆轮的往复振荡。',
stats:[['类型','瑞士叉瓦式',''],['宝石','2 颗红宝石',''],['频率','5 Hz','']]
});
return fork;
}
const palletFork = buildEscapement();
/* --- 摆轮 + 游丝 --- */
function buildBalanceWheel(){
const g = new THREE.Group();
// 摆动组:仅轮缘/轮辐/螺丝/中心轴/游丝随摆动,调节件保持静止
const osc = new THREE.Group();
g.add(osc);
const RR = 0.95;
// 摆轮外环(金色)
const ring = new THREE.Mesh(new THREE.TorusGeometry(RR,0.09,18,64), M.goldPolish);
ring.rotation.x = -Math.PI/2; // 水平放置
osc.add(ring);
// 摆轮轮辐(3条,减轻重量)
for(let i=0;i<3;i++){
const spoke = new THREE.Mesh(new THREE.BoxGeometry(RR*2-0.1,0.05,0.08), M.gold);
spoke.rotation.y = i*Math.PI/3;
osc.add(spoke);
}
// 配重螺丝(摆轮边缘的微调螺丝,径向拧入轮缘)
const _up = new THREE.Vector3(0,1,0);
for(let i=0;i<8;i++){
const a = i/8*Math.PI*2;
const w = new THREE.Mesh(new THREE.CylinderGeometry(0.07,0.07,0.18,12), M.bluedSteel);
w.position.set(Math.cos(a)*RR,0,Math.sin(a)*RR);
// 使圆柱轴向沿径向
w.quaternion.setFromUnitVectors(_up, new THREE.Vector3(Math.cos(a),0,Math.sin(a)));
osc.add(w);
}
// 中心轴(摆轴,垂直)
const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.34,16), M.steel);
osc.add(hub);
// 游丝(螺旋线)—— 使用 TubeGeometry
const pts = [];
const turns = 11, seg = 240;
for(let i=0;i<=seg;i++){
const t = i/seg * turns * Math.PI*2;
const rr = 0.2 + (0.6-0.2)*(i/seg);
pts.push(new THREE.Vector3(Math.cos(t)*rr, 0.22, Math.sin(t)*rr));
}
const curve = new THREE.CatmullRomCurve3(pts);
const hairspring = new THREE.Mesh(new THREE.TubeGeometry(curve, 300, 0.018, 6, false),
new THREE.MeshPhysicalMaterial({color:0x3a5fff, metalness:1, roughness:0.15, envMap:envMap, envMapIntensity:1.6, clearcoat:0.7}));
osc.add(hairspring);
// 游丝外端固定桩(垂直立柱,固定于夹板,不随摆轮转动)
const stud = new THREE.Mesh(new THREE.CylinderGeometry(0.07,0.07,0.2,10), M.goldPolish);
stud.position.set(0.62,0.1,0); g.add(stud);
// 快慢针(调节器,固定)
const index = new THREE.Mesh(new THREE.BoxGeometry(0.25,0.04,0.05), M.bluedSteel);
index.position.set(0.4,0.2,0); g.add(index);
g.position.set(-1.7,0,-1.75);
movement.add(g);
g.userData.osc = osc;
markPart(g,{
tag:'BALANCE WHEEL', title:'摆轮 + 游丝', role:'时间基准 / Oscillator 5 Hz',
desc:'腕表的“心脏”,决定走时精度。摆轮以 5 Hz(每小时 36,000 次)高频振荡,蓝钢游丝提供等时性回复力。边缘的蓝钢螺丝用于精密调校。每秒 10 次半振荡,画面中清晰可见。',
stats:[['频率','5 Hz · 36,000 vph',''],['游丝','蓝钢',''],['宝石','防震 Incabloc','']]
});
return g;
}
const balanceWheel = buildBalanceWheel();
/* ============================================================
指针(在机芯之上,玻璃之下)
============================================================ */
function buildHands(){
const g = new THREE.Group();
// 时针
const hour = new THREE.Mesh(new THREE.BoxGeometry(0.14,0.04,2.4), M.lume);
hour.geometry.translate(0,0,1.0); // 顶端伸出
hour.position.set(0,0.16,0);
hour.castShadow=true;
g.add(hour);
// 时针针尖菱形
const hTip = new THREE.Mesh(new THREE.ConeGeometry(0.09,0.3,4), M.lume);
hTip.rotation.x=-Math.PI/2; hTip.position.set(0,0.16,2.4);
g.add(hTip);
// 分针
const min = new THREE.Mesh(new THREE.BoxGeometry(0.1,0.04,3.5), M.lume);
min.geometry.translate(0,0,1.55);
min.position.set(0,0.2,0);
g.add(min);
const mTip = new THREE.Mesh(new THREE.ConeGeometry(0.07,0.25,4), M.lume);
mTip.rotation.x=-Math.PI/2; mTip.position.set(0,0.2,3.5);
g.add(mTip);
// 秒针(细钢针)
const sec = new THREE.Mesh(new THREE.CylinderGeometry(0.02,0.02,4.2,8), M.bluedSteel);
sec.rotation.x=-Math.PI/2; sec.geometry.translate(0,0,0);
sec.position.set(0,0.24,1.7);
g.add(sec);
// 秒针配重
const secW = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.06,16), M.bluedSteel);
secW.position.set(0,0.24,-0.5); g.add(secW);
// 中心轴帽
const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.12,0.14,0.12,16), M.goldPolish);
cap.position.y=0.28; g.add(cap);
watch.add(g);
return {g,hour,min,sec};
}
const hands = buildHands();
/* ============================================================
后处理
============================================================ */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene,camera));
const bloom = new UnrealBloomPass(new THREE.Vector2(innerWidth,innerHeight), 0.7, 0.7, 0.85);
composer.addPass(bloom);
const outPass = new OutputPass();
composer.addPass(outPass);
/* ============================================================
爆炸视图目标位置
============================================================ */
function setExplodeTargets(){
// 纯增量偏移(爆炸时叠加到原始位置上)
bridgeGroup.userData.explode = new THREE.Vector3(0,1.4,0);
balanceWheel.userData.explode = new THREE.Vector3(0,1.2,0);
palletFork.userData.explode = new THREE.Vector3(0,1.0,0);
wheels.forEach((w,i)=>{ w.userData.explode = new THREE.Vector3(0, 0.7+i*0.22, 0); });
mainspring.g.userData.explode = new THREE.Vector3(0,2.2,0);
plateGroup.userData.explode = new THREE.Vector3(0,-1.1,0);
caseParts.userData.explode = new THREE.Vector3(0,-0.6,0); // 表壳下沉
}
setExplodeTargets();
/* ============================================================
交互:射线检测 / 标签 / 信息面板
============================================================ */
const tagEl = document.createElement('div');
tagEl.className='tag3d'; tagEl.innerHTML='<div class="dot"></div><div class="line"></div><div class="lbl"></div>';
app.appendChild(tagEl);
function onPointerMove(e){
mouse.x = (e.clientX/innerWidth)*2-1;
mouse.y = -(e.clientY/innerHeight)*2+1;
}
function onPointerLeave(){
mouse.set(-2,-2);
tagEl.classList.remove('show');
if(hoverGroup){ setHighlight(hoverGroup,false); hoverGroup=null; }
renderer.domElement.classList.remove('cursor-pointer');
}
renderer.domElement.addEventListener('pointermove', onPointerMove);
renderer.domElement.addEventListener('pointerleave', onPointerLeave);
renderer.domElement.addEventListener('click', ()=>{
if(hoverGroup){ showInfo(hoverGroup.userData.info); }
});
function checkHover(){
if(state.exploded) return; // 爆炸时暂停高亮避免混乱
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(interactive, true);
if(hits.length){
const top = hits[0].object.userData.parentGroup || hoverGroup;
if(top !== hoverGroup){
if(hoverGroup) setHighlight(hoverGroup,false);
hoverGroup = top; setHighlight(hoverGroup,true);
}
// 更新标签
const pos = hits[0].point.clone();
const info = hoverGroup.userData.info;
tagEl.querySelector('.lbl').textContent = info.title + ' · ' + info.tag;
projectTag(pos);
tagEl.classList.add('show');
renderer.domElement.classList.add('cursor-pointer');
} else {
if(hoverGroup){ setHighlight(hoverGroup,false); hoverGroup=null; }
tagEl.classList.remove('show');
renderer.domElement.classList.remove('cursor-pointer');
}
}
function projectTag(pos3){
const v = pos3.clone().project(camera);
const x = (v.x*0.5+0.5)*innerWidth;
const y = (-v.y*0.5+0.5)*innerHeight - 40;
tagEl.style.left = x+'px'; tagEl.style.top = y+'px';
}
const infoEl = document.getElementById('info');
function showInfo(info){
document.getElementById('iTag').textContent = info.tag;
document.getElementById('iTitle').textContent = info.title;
document.getElementById('iRole').textContent = info.role;
document.getElementById('iDesc').textContent = info.desc;
const st = document.getElementById('iStats');
st.innerHTML = '';
info.stats.forEach(s=>{
const d = document.createElement('div'); d.className='stat';
d.innerHTML = `<div class="k">${s[0]}</div><div class="v">${s[1]}</div>`;
st.appendChild(d);
});
infoEl.classList.remove('hidden');
}
document.getElementById('closeInfo').addEventListener('click', ()=>infoEl.classList.add('hidden'));
/* ============================================================
UI 控件绑定
============================================================ */
function bindToggle(id, key, onChange){
const el = document.getElementById(id);
el.addEventListener('click', ()=>{
state[key]=!state[key]; el.classList.toggle('on',state[key]);
onChange&&onChange(state[key]);
});
}
bindToggle('tAuto','autoRotate',v=>{controls.autoRotate=v;});
bindToggle('tRun','movement');
bindToggle('tRefl','reflection',v=>{ floor.visible=v; scene.environment = v?envMap:null; scene.background = v? null : new THREE.Color(0x07080b); });
bindToggle('tBloom','bloom',v=>{bloom.enabled=v;});
document.getElementById('bExplode').addEventListener('click', ()=>{
state.exploded = !state.exploded;
document.getElementById('bExplode').classList.toggle('on',state.exploded);
});
document.getElementById('bReset').addEventListener('click', ()=>{
camera.position.set(0,6.5,13);
controls.target.set(0,0,0);
controls.update();
});
window.addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
composer.setSize(innerWidth,innerHeight);
});
/* ============================================================
时钟显示(底部)
============================================================ */
function tick(){
const d = new Date();
document.getElementById('hh').textContent = String(d.getHours()).padStart(2,'0');
document.getElementById('mm').textContent = String(d.getMinutes()).padStart(2,'0');
document.getElementById('ss').textContent = String(d.getSeconds()).padStart(2,'0');
}
setInterval(tick,1000); tick();
/* ============================================================
动画循环
============================================================ */
const targetPos = new THREE.Vector3();
const currentPos = new THREE.Vector3();
const origPos = new Map();
interactive.forEach(g=>origPos.set(g, g.position.clone()));
origPos.set(caseParts, caseParts.position.clone());
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(),0.05);
const t = clock.elapsedTime;
// 爆炸插值
const tgt = state.exploded?1:0;
state.explodeT += (tgt - state.explodeT) * Math.min(1, dt*4);
interactive.forEach(g=>{
const o = origPos.get(g); const e = g.userData.explode;
if(e){ g.position.lerpVectors(o, new THREE.Vector3(o.x+e.x,o.y+e.y,o.z+e.z), state.explodeT); }
});
// 表壳下沉
const co = origPos.get(caseParts);
caseParts.position.y = co.y + (-0.6) * state.explodeT;
// 机芯运转
if(state.movement && state.explodeT<0.5){
const f = state.rate * dt;
wheels.forEach(w=>{ w.rotation.y -= f * w.userData.spin * 0.05; });
mainspring.g.rotation.y += f*0.015; // 发条盒缓慢释放
palletFork.rotation.y = (palletFork.userData.baseY||1.15) + Math.sin(t*state.rate*Math.PI*2*0.4)*0.14;
(balanceWheel.userData.osc||balanceWheel).rotation.y = Math.sin(t*state.rate*Math.PI*2*0.2)*0.6; // 摆动
}
// 爆炸时抬升指针,避免与机芯重叠
hands.g.position.y = 1.6 * state.explodeT;
// 指针随真实时间(12 点 = +Z,顺时针为 +Y 旋转)
const d = new Date();
const sec = d.getSeconds()+d.getMilliseconds()/1000;
const min = d.getMinutes()+sec/60;
const hr = (d.getHours()%12)+min/60;
hands.sec.rotation.y = sec/60*Math.PI*2;
hands.min.rotation.y = min/60*Math.PI*2;
hands.hour.rotation.y = hr/12*Math.PI*2;
// accent light 微动
accentPoint.position.x = Math.sin(t*0.5)*2;
accentPoint.position.z = Math.cos(t*0.5)*2+3;
controls.update();
checkHover();
composer.render();
}
// 隐藏 loader 并启动
setTimeout(()=>{
const l=document.getElementById('loader');
l.style.opacity='0'; setTimeout(()=>l.remove(),800);
}, 600);
animate();
</script>
</body>
</html>