Files
nagi-bench/outputs/kimi-k2-7-code/kimi-code-thinking/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

1024 lines
40 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>镂空机械表 · 3D 交互展示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--gold: #d4af37;
--rose: #c27e60;
--steel: #a8b5c4;
--lume: #c8ffaa;
--glass: rgba(20, 22, 26, 0.72);
--glass-border: rgba(255, 255, 255, 0.10);
}
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: radial-gradient(circle at 50% 35%, #1f242c 0%, #0d1014 55%, #050608 100%);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #f0f0f0;
user-select: none;
}
#canvas-container {
position: absolute; inset: 0;
z-index: 1;
}
#loading {
position: absolute; inset: 0; z-index: 20;
display: flex; align-items: center; justify-content: center;
background: #050608; color: var(--gold);
font-size: 1.2rem; letter-spacing: 0.2em;
transition: opacity 0.8s ease;
}
#loading.hidden { opacity: 0; pointer-events: none; }
/* UI Overlay */
.ui-layer {
position: absolute; z-index: 10;
pointer-events: none;
}
.title-card {
top: 26px; left: 30px;
pointer-events: auto;
}
.title-card h1 {
font-size: 1.55rem; font-weight: 300;
letter-spacing: 0.18em; text-transform: uppercase;
background: linear-gradient(90deg, #fff 0%, var(--gold) 55%, #fff 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-shadow: 0 2px 18px rgba(212,175,55,0.25);
}
.title-card p {
margin-top: 6px; font-size: 0.78rem;
color: rgba(255,255,255,0.55); letter-spacing: 0.08em;
}
/* Controls */
.controls {
bottom: 28px; left: 50%;
transform: translateX(-50%);
display: flex; gap: 10px;
pointer-events: auto;
padding: 8px 12px;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 999px;
backdrop-filter: blur(14px);
box-shadow: 0 14px 40px rgba(0,0,0,0.45);
}
.btn {
display: flex; align-items: center; gap: 7px;
padding: 9px 16px; border-radius: 999px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
color: rgba(255,255,255,0.9);
font-size: 0.8rem; cursor: pointer;
transition: all 0.25s ease;
white-space: nowrap;
}
.btn:hover {
background: rgba(212,175,55,0.14);
border-color: rgba(212,175,55,0.35);
transform: translateY(-1px);
}
.btn.active {
background: rgba(212,175,55,0.22);
border-color: rgba(212,175,55,0.55);
color: #fff;
box-shadow: 0 0 18px rgba(212,175,55,0.18);
}
/* Info Panel */
#info-panel {
top: 26px; right: 30px; width: 300px;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 18px;
backdrop-filter: blur(16px);
padding: 22px;
pointer-events: auto;
box-shadow: 0 18px 50px rgba(0,0,0,0.45);
transform: translateX(30px);
opacity: 0; transition: all 0.4s cubic-bezier(0.16,1,0.3,1);
}
#info-panel.visible { transform: translateX(0); opacity: 1; }
#info-panel .part-icon {
width: 46px; height: 46px; border-radius: 12px;
background: linear-gradient(135deg, rgba(212,175,55,0.25), rgba(194,126,96,0.15));
border: 1px solid rgba(212,175,55,0.25);
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem; margin-bottom: 14px;
}
#info-panel h2 {
font-size: 1.15rem; font-weight: 500; margin-bottom: 6px;
letter-spacing: 0.04em;
}
#info-panel .meta {
font-size: 0.7rem; color: rgba(255,255,255,0.45);
text-transform: uppercase; letter-spacing: 0.12em;
margin-bottom: 14px;
}
#info-panel p {
font-size: 0.86rem; line-height: 1.65;
color: rgba(255,255,255,0.72);
}
#info-panel .close {
position: absolute; top: 14px; right: 16px;
width: 28px; height: 28px; border-radius: 50%;
border: none; background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.7); cursor: pointer;
font-size: 1rem; line-height: 1;
transition: background 0.2s;
}
#info-panel .close:hover { background: rgba(255,255,255,0.18); }
/* Floating label */
#tooltip {
position: absolute; z-index: 12;
padding: 7px 12px;
background: rgba(10,12,15,0.82);
border: 1px solid rgba(212,175,55,0.35);
border-radius: 8px;
color: #fff; font-size: 0.8rem;
pointer-events: none; opacity: 0;
transform: translate(-50%, -130%);
transition: opacity 0.18s ease;
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
#tooltip::after {
content: ''; position: absolute;
bottom: -5px; left: 50%; transform: translateX(-50%);
border-width: 5px 5px 0; border-style: solid;
border-color: rgba(212,175,55,0.5) transparent transparent;
}
/* Stats / helper text */
.helper {
bottom: 92px; left: 50%;
transform: translateX(-50%);
font-size: 0.72rem; color: rgba(255,255,255,0.38);
letter-spacing: 0.06em;
text-align: center;
}
/* Part label lines overlay */
#labels-overlay {
position: absolute; inset: 0; z-index: 9;
pointer-events: none;
}
.part-label {
position: absolute;
padding: 4px 10px;
background: rgba(10,12,15,0.75);
border: 1px solid rgba(212,175,55,0.3);
border-radius: 6px;
color: rgba(255,255,255,0.9);
font-size: 0.72rem;
transform: translate(-50%, -50%);
opacity: 0; transition: opacity 0.25s ease;
pointer-events: none;
}
.part-label.active { opacity: 1; }
@media (max-width: 720px) {
.title-card { left: 16px; top: 16px; }
.title-card h1 { font-size: 1.15rem; }
#info-panel { width: calc(100% - 32px); right: 16px; top: auto; bottom: 100px; }
.controls { bottom: 16px; flex-wrap: wrap; justify-content: center; width: calc(100% - 32px); border-radius: 20px; }
}
</style>
</head>
<body>
<div id="loading">ASSEMBLING MOVEMENT…</div>
<div id="canvas-container"></div>
<div class="ui-layer title-card">
<h1>Skeleton Tourbillon</h1>
<p>镂空机械机芯 · 实时交互展示</p>
</div>
<div class="ui-layer helper">
拖拽旋转 · 滚轮缩放 · 悬停查看零件 · 点击显示详情
</div>
<div class="ui-layer controls">
<button class="btn" id="btn-explode" title="切换爆炸视图">💥 爆炸视图</button>
<button class="btn" id="btn-auto" title="自动旋转">🔄 自动旋转</button>
<button class="btn active" id="btn-lume" title="切换夜光">✨ 夜光效果</button>
<button class="btn" id="btn-reset" title="重置视角">⌂ 重置</button>
</div>
<div id="info-panel">
<button class="close" id="info-close">×</button>
<div class="part-icon" id="info-icon">⚙️</div>
<h2 id="info-title">零件名称</h2>
<div class="meta" id="info-meta">COMPONENT</div>
<p id="info-desc">点击零件查看详细说明。</p>
</div>
<div id="tooltip">零件</div>
<div id="labels-overlay"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.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';
// ===== Configuration =====
const CONFIG = {
scale: 1,
lumeIntensity: 2.2,
bloomStrength: 0.08,
bloomRadius: 0.55,
bloomThreshold: 0.88,
autoRotateSpeed: 0.35,
explodeDistance: 2.4,
shadowMapSize: 2048
};
// ===== State =====
const state = {
exploded: false,
autoRotate: false,
lumeOn: true,
hovered: null,
selected: null,
paused: false,
explodeFactor: 0
};
// ===== Scene setup =====
const container = document.getElementById('canvas-container');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0c0f);
scene.fog = new THREE.FogExp2(0x0a0c0f, 0.035);
const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 14);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.85;
container.appendChild(renderer.domElement);
// Environment for reflections
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
const roomEnv = new RoomEnvironment();
scene.environment = pmremGenerator.fromScene(roomEnv).texture;
roomEnv.dispose();
// Post-processing
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
CONFIG.bloomStrength, CONFIG.bloomRadius, CONFIG.bloomThreshold
);
composer.addPass(bloomPass);
const outputPass = new OutputPass();
composer.addPass(outputPass);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.minDistance = 5;
controls.maxDistance = 22;
controls.autoRotate = false;
controls.autoRotateSpeed = CONFIG.autoRotateSpeed;
// ===== Lighting =====
const ambient = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambient);
const keyLight = new THREE.SpotLight(0xfff4e0, 8);
keyLight.position.set(14, 18, 13);
keyLight.angle = Math.PI / 6;
keyLight.penumbra = 0.65;
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(CONFIG.shadowMapSize, CONFIG.shadowMapSize);
keyLight.shadow.bias = -0.0001;
scene.add(keyLight);
const fillLight = new THREE.PointLight(0xc8e0ff, 4);
fillLight.position.set(-10, 4, 8);
scene.add(fillLight);
const rimLight = new THREE.SpotLight(0xffdca8, 5);
rimLight.position.set(0, -14, -10);
rimLight.lookAt(0, 0, 0);
scene.add(rimLight);
const lumeLight = new THREE.PointLight(0x88ff66, 2, 6);
lumeLight.position.set(0, 0, 1.2);
scene.add(lumeLight);
// ===== Materials =====
const mat = {
roseGold: new THREE.MeshStandardMaterial({
color: 0xc27e60, metalness: 1.0, roughness: 0.20,
envMapIntensity: 0.9
}),
yellowGold: new THREE.MeshStandardMaterial({
color: 0xd4af37, metalness: 1.0, roughness: 0.18,
envMapIntensity: 0.95
}),
steel: new THREE.MeshStandardMaterial({
color: 0xb0becd, metalness: 1.0, roughness: 0.22,
envMapIntensity: 0.85
}),
brass: new THREE.MeshStandardMaterial({
color: 0xb8a85c, metalness: 1.0, roughness: 0.26,
envMapIntensity: 0.85
}),
rhodium: new THREE.MeshStandardMaterial({
color: 0xe8ecf0, metalness: 1.0, roughness: 0.16,
envMapIntensity: 0.9
}),
darkMetal: new THREE.MeshStandardMaterial({
color: 0x2a2f36, metalness: 0.85, roughness: 0.35,
envMapIntensity: 0.9
}),
sapphire: new THREE.MeshPhysicalMaterial({
color: 0xaaccff, metalness: 0, roughness: 0.02,
transmission: 0.92, thickness: 1.2,
ior: 1.76, transparent: true, opacity: 0.32,
envMapIntensity: 0.45, clearcoat: 0.6, clearcoatRoughness: 0.02
}),
ruby: new THREE.MeshPhysicalMaterial({
color: 0xd6004e, metalness: 0.2, roughness: 0.04,
transmission: 0.25, thickness: 0.6,
clearcoat: 1.0, envMapIntensity: 0.8
}),
lume: new THREE.MeshStandardMaterial({
color: 0xeaffdd, emissive: 0x66ff44,
emissiveIntensity: CONFIG.lumeIntensity,
roughness: 0.6, metalness: 0
}),
lumeDim: new THREE.MeshStandardMaterial({
color: 0xc8d4b8, emissive: 0x224411,
emissiveIntensity: 0.15,
roughness: 0.7, metalness: 0
}),
dialBlack: new THREE.MeshStandardMaterial({
color: 0x15181c, metalness: 0.7, roughness: 0.4,
envMapIntensity: 0.8
}),
screw: new THREE.MeshStandardMaterial({
color: 0xdddddd, metalness: 1.0, roughness: 0.15
})
};
// ===== Helpers =====
const DEG = Math.PI / 180;
const watchGroup = new THREE.Group();
scene.add(watchGroup);
const parts = []; // raycast targets
const gears = []; // animated gears
const oscillators = []; // balance wheels
const hands = {}; // hour/minute/second
const explodeParts = []; // parts that can explode
function registerPart(mesh, info, explodeAxis = new THREE.Vector3(0, 0, 1), explodeMult = 1) {
mesh.userData = {
...mesh.userData,
isPart: true,
info,
originalParent: mesh.parent,
homePos: mesh.position.clone(),
explodeDir: explodeAxis.clone().normalize(),
explodeMult
};
parts.push(mesh);
explodeParts.push(mesh);
return mesh;
}
function makeGearShape(radius, teeth, holeRadius = 0) {
const shape = new THREE.Shape();
const innerR = radius * 0.82;
const outerR = radius;
const tipR = radius * 1.12;
const step = (Math.PI * 2) / teeth;
for (let i = 0; i < teeth; i++) {
const a = i * step;
const a0 = a - step * 0.22;
const a1 = a - step * 0.10;
const a2 = a + step * 0.10;
const a3 = a + step * 0.22;
if (i === 0) shape.moveTo(Math.cos(a0) * innerR, Math.sin(a0) * innerR);
else shape.lineTo(Math.cos(a0) * innerR, Math.sin(a0) * innerR);
shape.lineTo(Math.cos(a1) * outerR, Math.sin(a1) * outerR);
shape.lineTo(Math.cos(a2) * tipR, Math.sin(a2) * tipR);
shape.lineTo(Math.cos(a3) * outerR, Math.sin(a3) * outerR);
}
shape.closePath();
if (holeRadius > 0) {
const hole = new THREE.Path();
hole.absarc(0, 0, holeRadius, 0, Math.PI * 2, true);
shape.holes.push(hole);
}
return shape;
}
function createGear(radius, teeth, thickness, material, holeRadius = 0, name = '齿轮') {
const shape = makeGearShape(radius, teeth, holeRadius);
const geom = new THREE.ExtrudeGeometry(shape, {
depth: thickness, bevelEnabled: true, bevelThickness: 0.03,
bevelSize: 0.02, bevelSegments: 2, curveSegments: Math.min(24, teeth * 2)
});
geom.center();
const mesh = new THREE.Mesh(geom, material);
mesh.castShadow = true; mesh.receiveShadow = true;
registerPart(mesh, { name, category: '齿轮系', desc: '将动力从发条盒传递至擒纵机构,并驱动指针运转。' });
return mesh;
}
function createSpiralTexture() {
const cvs = document.createElement('canvas');
cvs.width = 512; cvs.height = 512;
const ctx = cvs.getContext('2d');
ctx.fillStyle = '#1a1510';
ctx.fillRect(0, 0, 512, 512);
ctx.strokeStyle = '#6a5a3a';
ctx.lineWidth = 2;
ctx.beginPath();
const cx = 256, cy = 256;
for (let i = 0; i < 720; i++) {
const a = i * 0.08;
const r = 10 + i * 0.32;
const x = cx + Math.cos(a) * r;
const y = cy + Math.sin(a) * r;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
const tex = new THREE.CanvasTexture(cvs);
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
return tex;
}
// ===== Build watch =====
function buildWatch() {
const caseRadius = 3.6;
const caseThickness = 1.15;
// 1. Case ring
const caseShape = new THREE.Shape();
caseShape.absarc(0, 0, caseRadius, 0, Math.PI * 2, false);
const caseHole = new THREE.Path();
caseHole.absarc(0, 0, caseRadius - 0.32, 0, Math.PI * 2, true);
caseShape.holes.push(caseHole);
const caseGeom = new THREE.ExtrudeGeometry(caseShape, {
depth: caseThickness, bevelEnabled: true, bevelThickness: 0.12,
bevelSize: 0.12, bevelSegments: 4, curveSegments: 64
});
caseGeom.center();
const caseMesh = new THREE.Mesh(caseGeom, mat.roseGold);
caseMesh.castShadow = true; caseMesh.receiveShadow = true;
registerPart(caseMesh, { name: '表壳', category: '外观', desc: '18K 玫瑰金表壳,经抛光与拉丝交替处理,承载整枚机芯。' }, new THREE.Vector3(0,0,1), 0.6);
watchGroup.add(caseMesh);
// Lugs (simplified)
for (let i = 0; i < 4; i++) {
const angle = i * 90 * DEG;
const lug = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.1, caseThickness * 0.85), mat.roseGold);
lug.position.set(Math.cos(angle) * (caseRadius + 0.25), Math.sin(angle) * (caseRadius + 0.25), 0);
lug.rotation.z = angle;
lug.castShadow = true;
registerPart(lug, { name: '表耳', category: '外观', desc: '连接表壳与表带的弧形表耳,符合人体工学。' }, new THREE.Vector3(Math.cos(angle), Math.sin(angle), 0), 0.4);
watchGroup.add(lug);
}
// 2. Sapphire crystal
const crystalGeom = new THREE.CylinderGeometry(caseRadius - 0.05, caseRadius - 0.05, 0.12, 64);
const crystal = new THREE.Mesh(crystalGeom, mat.sapphire);
crystal.position.z = caseThickness / 2 + 0.18;
crystal.rotation.x = Math.PI / 2;
registerPart(crystal, { name: '蓝宝石水晶表镜', category: '外观', desc: '双面防反射涂层蓝宝石水晶,莫氏硬度 9 级。' }, new THREE.Vector3(0,0,1), 1.2);
watchGroup.add(crystal);
const caseBack = crystal.clone();
caseBack.position.z = -(caseThickness / 2 + 0.18);
caseBack.material = mat.sapphire;
registerPart(caseBack, { name: '透底蓝宝石底盖', category: '外观', desc: '蓝宝石水晶透底,可欣赏机芯背面打磨。' }, new THREE.Vector3(0,0,-1), 1.2);
watchGroup.add(caseBack);
// 3. Chapter ring / dial
const ringShape = new THREE.Shape();
ringShape.absarc(0, 0, caseRadius - 0.42, 0, Math.PI * 2, false);
const ringHole = new THREE.Path();
ringHole.absarc(0, 0, caseRadius - 0.72, 0, Math.PI * 2, true);
ringShape.holes.push(ringHole);
const ringGeom = new THREE.ExtrudeGeometry(ringShape, { depth: 0.08, bevelEnabled: false, curveSegments: 64 });
ringGeom.center();
const chapterRing = new THREE.Mesh(ringGeom, mat.darkMetal);
chapterRing.position.z = caseThickness / 2 - 0.05;
registerPart(chapterRing, { name: '分钟刻度圈', category: '表盘', desc: '哑光黑金属刻度圈,标注分钟刻度。' }, new THREE.Vector3(0,0,1), 0.35);
watchGroup.add(chapterRing);
// Hour markers
for (let i = 0; i < 12; i++) {
const a = i * 30 * DEG;
const marker = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.42, 0.06), mat.lume);
marker.position.set(Math.cos(a) * (caseRadius - 0.55), Math.sin(a) * (caseRadius - 0.55), caseThickness / 2 - 0.02);
marker.rotation.z = a;
marker.userData.isLume = true;
registerPart(marker, { name: '夜光时标', category: '表盘', desc: 'Super-LumiNova 夜光材料,暗光下清晰读时。' }, new THREE.Vector3(0,0,1), 0.45);
watchGroup.add(marker);
}
// 4. Movement plates (front & back) with cutouts
function createPlate(z, thickness, matPlate, name) {
const shape = new THREE.Shape();
shape.absarc(0, 0, caseRadius - 0.38, 0, Math.PI * 2, false);
const cutouts = [
{ r: 1.05, x: 0, y: 0 },
{ r: 0.82, x: 1.55, y: 0.7 },
{ r: 0.72, x: -1.35, y: 0.85 },
{ r: 0.68, x: 0.2, y: -1.45 },
{ r: 0.55, x: -1.1, y: -0.9 }
];
cutouts.forEach(c => {
const h = new THREE.Path();
h.absarc(c.x, c.y, c.r, 0, Math.PI * 2, true);
shape.holes.push(h);
});
const g = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: true, bevelThickness: 0.04, bevelSize: 0.04, bevelSegments: 2, curveSegments: 64 });
g.center();
const plate = new THREE.Mesh(g, matPlate);
plate.position.z = z;
plate.castShadow = true; plate.receiveShadow = true;
registerPart(plate, { name, category: '机芯', desc: '镂空主夹板/桥板,经倒角与日内瓦条纹打磨。' }, new THREE.Vector3(0,0,z > 0 ? 1 : -1), 0.75);
watchGroup.add(plate);
return plate;
}
createPlate(caseThickness/2 - 0.22, 0.14, mat.rhodium, '前夹板');
createPlate(-caseThickness/2 + 0.22, 0.14, mat.rhodium, '后夹板');
// 5. Gear train
const gearPositions = [
{ x: 0, y: 0, r: 0.92, teeth: 60, thick: 0.16, mat: mat.brass, hole: 0.16, name: '中心轮', rate: 1/3600, desc: '中心轮每小时旋转一圈,直接驱动分针。' },
{ x: 1.55, y: 0.7, r: 0.72, teeth: 48, thick: 0.14, mat: mat.steel, hole: 0.12, name: '三轮', rate: -4, desc: '第三轮将动力从中心轮传向四轮。' },
{ x: -1.35, y: 0.85, r: 0.68, teeth: 42, thick: 0.14, mat: mat.steel, hole: 0.12, name: '四轮', rate: 12, desc: '第四轮每分钟旋转一圈,驱动秒针。' },
{ x: 0.2, y: -1.45, r: 0.55, teeth: 36, thick: 0.12, mat: mat.yellowGold, hole: 0.10, name: '擒纵轮', rate: -48, desc: '擒纵轮控制能量释放节奏,是机械表的"心跳"来源。' },
{ x: -1.1, y: -0.9, r: 1.05, teeth: 70, thick: 0.22, mat: mat.yellowGold, hole: 0.18, name: '发条盒', rate: 0.05, desc: '储存主发条动力,是机芯的能量来源。' }
];
gearPositions.forEach(g => {
const gear = createGear(g.r, g.teeth, g.thick, g.mat, g.hole, g.name);
gear.position.set(g.x, g.y, 0);
gear.userData.info.desc = g.desc;
gear.userData.rate = g.rate;
gears.push(gear);
watchGroup.add(gear);
// Ruby bearing on top
const jewel = new THREE.Mesh(new THREE.CylinderGeometry(g.hole * 1.6, g.hole * 1.6, g.thick + 0.06, 16), mat.ruby);
jewel.rotation.x = Math.PI / 2;
jewel.position.set(g.x, g.y, g.thick/2 + 0.04);
registerPart(jewel, { name: '红宝石轴承', category: '机芯', desc: '人造红宝石轴承,减少摩擦并提高耐磨性。' }, new THREE.Vector3(0,0,1), 0.55);
watchGroup.add(jewel);
const jewelBack = jewel.clone();
jewelBack.position.z = -(g.thick/2 + 0.04);
registerPart(jewelBack, { name: '红宝石轴承', category: '机芯', desc: '人造红宝石轴承,减少摩擦并提高耐磨性。' }, new THREE.Vector3(0,0,-1), 0.55);
watchGroup.add(jewelBack);
});
// Mainspring barrel drum
const barrelTex = createSpiralTexture();
const barrelMat = new THREE.MeshStandardMaterial({ map: barrelTex, color: 0x8a7a50, metalness: 0.8, roughness: 0.35 });
const barrelDrum = new THREE.Mesh(new THREE.CylinderGeometry(1.0, 1.0, 0.26, 48, 1, true), barrelMat);
barrelDrum.rotation.x = Math.PI / 2;
barrelDrum.position.set(-1.1, -0.9, 0);
barrelDrum.castShadow = true;
registerPart(barrelDrum, { name: '发条盒鼓', category: '动力', desc: '内含盘绕的主发条,储存约 40 小时动力。' }, new THREE.Vector3(-0.3,-0.2,0), 0.6);
watchGroup.add(barrelDrum);
// Barrel cover
const barrelCover = new THREE.Mesh(new THREE.CylinderGeometry(1.0, 1.0, 0.02, 48), mat.yellowGold);
barrelCover.rotation.x = Math.PI / 2;
barrelCover.position.set(-1.1, -0.9, 0.13);
registerPart(barrelCover, { name: '发条盒盖', category: '动力', desc: '抛光黄铜发条盒盖,刻有品牌徽标区域。' }, new THREE.Vector3(0,0,1), 0.55);
watchGroup.add(barrelCover);
// 6. Balance wheel + hairspring
function createBalanceWheel(x, y) {
const bwGroup = new THREE.Group();
bwGroup.position.set(x, y, 0.45);
// Rim
const rimShape = new THREE.Shape();
rimShape.absarc(0, 0, 0.85, 0, Math.PI * 2, false);
const rimHole = new THREE.Path();
rimHole.absarc(0, 0, 0.72, 0, Math.PI * 2, true);
rimShape.holes.push(rimHole);
const rim = new THREE.Mesh(new THREE.ExtrudeGeometry(rimShape, { depth: 0.10, bevelEnabled: true, bevelThickness: 0.03, bevelSize: 0.02, bevelSegments: 2, curveSegments: 48 }), mat.rhodium);
rim.castShadow = true;
bwGroup.add(rim);
// Arms
for (let i = 0; i < 4; i++) {
const a = i * 45 * DEG;
const arm = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.82, 0.08), mat.rhodium);
arm.position.set(Math.cos(a) * 0.45, Math.sin(a) * 0.45, 0);
arm.rotation.z = a;
bwGroup.add(arm);
}
// Center screw
const screw = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.10, 0.18, 16), mat.screw);
screw.rotation.x = Math.PI / 2;
bwGroup.add(screw);
// Hairspring spiral
class SpiralCurve extends THREE.Curve {
constructor(turns) { super(); this.turns = turns; }
getPoint(t) {
const angle = t * this.turns * Math.PI * 2;
const r = 0.12 + t * 0.62;
return new THREE.Vector3(Math.cos(angle) * r, Math.sin(angle) * r, -0.02);
}
}
const spiral = new THREE.Mesh(new THREE.TubeGeometry(new SpiralCurve(3.5), 120, 0.018, 6, false), mat.steel);
bwGroup.add(spiral);
// Cock bridge over balance
const cockShape = new THREE.Shape();
cockShape.absarc(0, 0, 0.32, 0, Math.PI, false);
cockShape.lineTo(-0.5, 0.7); cockShape.lineTo(0.5, 0.7);
const cock = new THREE.Mesh(new THREE.ExtrudeGeometry(cockShape, { depth: 0.08, bevelEnabled: true, bevelThickness: 0.03, bevelSize: 0.02, bevelSegments: 2 }), mat.rhodium);
cock.position.set(x, y + 0.45, 0.72);
cock.castShadow = true;
registerPart(cock, { name: '摆轮夹板', category: '调速', desc: '固定摆轮与游丝的桥板,经手工倒角抛光。' }, new THREE.Vector3(0,0.3,1), 0.8);
watchGroup.add(cock);
// Ruby endstone
const endstone = new THREE.Mesh(new THREE.SphereGeometry(0.08, 16, 16), mat.ruby);
endstone.position.set(x, y + 0.45, 0.82);
registerPart(endstone, { name: '摆轮避震器宝石', category: '调速', desc: '保护摆轮轴尖免受冲击损伤的宝石轴承。' }, new THREE.Vector3(0,0,1), 0.9);
watchGroup.add(endstone);
registerPart(rim, { name: '摆轮', category: '调速', desc: '与游丝配合控制振动周期,决定走时精度。' }, new THREE.Vector3(0,0,1), 0.55);
registerPart(spiral, { name: '游丝', category: '调速', desc: '扁平游丝通过伸缩控制摆轮往复振荡。' }, new THREE.Vector3(0,0,1), 0.6);
oscillators.push({ group: bwGroup, phase: 0 });
watchGroup.add(bwGroup);
}
createBalanceWheel(0.2, -2.45);
// 7. Escapement pallet fork (simplified)
const palletShape = new THREE.Shape();
palletShape.moveTo(-0.15, -0.35);
palletShape.lineTo(0.15, -0.35);
palletShape.lineTo(0.08, 0.35);
palletShape.lineTo(-0.08, 0.35);
const pallet = new THREE.Mesh(new THREE.ExtrudeGeometry(palletShape, { depth: 0.10, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.015, bevelSegments: 2 }), mat.steel);
pallet.position.set(0.55, -1.85, 0.42);
pallet.castShadow = true;
registerPart(pallet, { name: '擒纵叉', category: '调速', desc: '在擒纵轮与摆轮之间传递脉冲,实现滴答声。' }, new THREE.Vector3(0.2,-0.3,1), 0.7);
watchGroup.add(pallet);
// Pallet jewels
const pj1 = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.12, 0.12), mat.ruby);
pj1.position.set(-0.12, -0.32, 0.05);
pallet.add(pj1);
const pj2 = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.12, 0.12), mat.ruby);
pj2.position.set(0.12, 0.32, 0.05);
pallet.add(pj2);
// 8. Hands
function createHand(width, length, colorMat, lumeMat, name) {
const g = new THREE.Group();
const shaft = new THREE.Mesh(new THREE.CylinderGeometry(width * 0.35, width * 0.35, length * 0.78, 12), colorMat);
shaft.position.y = length * 0.38;
shaft.rotation.x = 0;
g.add(shaft);
const tip = new THREE.Mesh(new THREE.BoxGeometry(width * 0.55, length * 0.35, 0.04), lumeMat);
tip.position.y = length * 0.62;
tip.userData.isLume = true;
g.add(tip);
const balance = new THREE.Mesh(new THREE.BoxGeometry(width, length * 0.22, 0.04), colorMat);
balance.position.y = -length * 0.08;
g.add(balance);
// Center pipe
const pipe = new THREE.Mesh(new THREE.CylinderGeometry(width * 0.4, width * 0.4, 0.25, 12), colorMat);
pipe.rotation.x = Math.PI / 2;
g.add(pipe);
return g;
}
hands.hour = createHand(0.16, 1.55, mat.yellowGold, mat.lume, '时针');
hands.minute = createHand(0.12, 2.25, mat.yellowGold, mat.lume, '分针');
hands.second = createHand(0.045, 2.55, mat.steel, mat.lume, '秒针');
hands.hour.position.z = caseThickness / 2 + 0.05;
hands.minute.position.z = caseThickness / 2 + 0.10;
hands.second.position.z = caseThickness / 2 + 0.15;
[hands.hour, hands.minute, hands.second].forEach((h, i) => {
h.userData = {
isPart: true,
info: { name: ['时针','分针','秒针'][i], category: '显示', desc: ['指示小时','指示分钟','指示秒钟,由四轮驱动'][i] },
homePos: h.position.clone(),
explodeDir: new THREE.Vector3(0, 0, 1),
explodeMult: 0.35
};
parts.push(h);
explodeParts.push(h);
watchGroup.add(h);
});
// 9. Screws on bridges
const screwPositions = [
[1.6, 1.5], [-2.0, 1.3], [1.8, -1.4], [-2.1, -1.5], [0.4, 1.4]
];
screwPositions.forEach(([x, y]) => {
const screwHead = new THREE.Mesh(new THREE.CylinderGeometry(0.10, 0.10, 0.06, 12), mat.screw);
screwHead.rotation.x = Math.PI / 2;
screwHead.position.set(x, y, caseThickness / 2 - 0.14);
// slot
const slot = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.025, 0.07), mat.darkMetal);
slot.position.set(x, y, caseThickness / 2 - 0.12);
registerPart(screwHead, { name: '蓝钢螺丝', category: '机芯', desc: '烤蓝螺丝,固定夹板并点缀机芯。' }, new THREE.Vector3(0,0,1), 0.6);
registerPart(slot, { name: '螺丝槽', category: '机芯', desc: '一字螺丝槽,用于拆装调校。' }, new THREE.Vector3(0,0,1), 0.6);
watchGroup.add(screwHead);
watchGroup.add(slot);
});
// 10. Crown
const crown = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.22, 0.35, 20), mat.roseGold);
crown.rotation.z = Math.PI / 2;
crown.position.set(caseRadius + 0.15, 0, 0);
crown.castShadow = true;
registerPart(crown, { name: '表冠', category: '外观', desc: '用于上链与调校时间的玫瑰金表冠。' }, new THREE.Vector3(1,0,0), 0.5);
watchGroup.add(crown);
// Center all nicely
watchGroup.rotation.x = -0.25;
watchGroup.rotation.y = 0.15;
}
buildWatch();
// ===== Labels overlay =====
const labelsData = [
{ name: '摆轮', objName: '摆轮' },
{ name: '擒纵轮', objName: '擒纵轮' },
{ name: '发条盒', objName: '发条盒' },
{ name: '蓝宝石表镜', objName: '蓝宝石水晶表镜' },
{ name: '红宝石轴承', objName: '红宝石轴承' }
];
const labelsOverlay = document.getElementById('labels-overlay');
const labelEls = [];
labelsData.forEach(item => {
const el = document.createElement('div');
el.className = 'part-label';
el.textContent = item.name;
labelsOverlay.appendChild(el);
labelEls.push({ el, name: item.objName });
});
function updateLabels() {
labelEls.forEach(le => {
const target = parts.find(p => p.userData.info && p.userData.info.name === le.name);
if (!target) return;
const pos = new THREE.Vector3();
target.getWorldPosition(pos);
pos.y += 0.55;
pos.project(camera);
const x = (pos.x * 0.5 + 0.5) * window.innerWidth;
const y = (-pos.y * 0.5 + 0.5) * window.innerHeight;
le.el.style.left = `${x}px`;
le.el.style.top = `${y}px`;
const visible = state.exploded || (state.selected && state.selected === target);
le.el.classList.toggle('active', visible && x > 0 && x < window.innerWidth && y > 0 && y < window.innerHeight);
});
}
// ===== Interaction =====
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const tooltip = document.getElementById('tooltip');
const infoPanel = document.getElementById('info-panel');
const infoTitle = document.getElementById('info-title');
const infoMeta = document.getElementById('info-meta');
const infoDesc = document.getElementById('info-desc');
const infoIcon = document.getElementById('info-icon');
function getIntersects(e) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
return raycaster.intersectObjects(parts, true);
}
function setHighlight(mesh, on) {
if (!mesh) return;
if (on) {
if (!mesh.userData._origMat) {
mesh.userData._origMat = mesh.material;
if (Array.isArray(mesh.material)) {
mesh.material = mesh.material.map(m => m.clone());
} else if (mesh.material) {
mesh.material = mesh.material.clone();
}
}
const m = mesh.material;
if (Array.isArray(m)) {
m.forEach(sub => { sub.emissive.setHex(0x44aaff); sub.emissiveIntensity = 0.85; });
} else if (m) {
m.emissive.setHex(0x44aaff); m.emissiveIntensity = 0.85;
}
} else {
if (mesh.userData._origMat) {
mesh.material = mesh.userData._origMat;
mesh.userData._origMat = null;
}
}
}
window.addEventListener('mousemove', e => {
const hits = getIntersects(e);
let hit = null;
if (hits.length) {
hit = hits[0].object;
while (hit && (!hit.userData || !hit.userData.isPart)) hit = hit.parent;
}
if (state.hovered && state.hovered !== hit && state.hovered !== state.selected) {
setHighlight(state.hovered, false);
}
state.hovered = hit;
if (hit) {
if (hit !== state.selected) setHighlight(hit, true);
document.body.style.cursor = 'pointer';
tooltip.style.opacity = 1;
tooltip.style.left = e.clientX + 'px';
tooltip.style.top = e.clientY + 'px';
tooltip.textContent = hit.userData.info.name;
} else {
document.body.style.cursor = 'default';
tooltip.style.opacity = 0;
}
});
window.addEventListener('click', e => {
const hits = getIntersects(e);
let hit = null;
if (hits.length) {
hit = hits[0].object;
while (hit && (!hit.userData || !hit.userData.isPart)) hit = hit.parent;
}
if (hit) {
if (state.selected && state.selected !== hit) setHighlight(state.selected, false);
state.selected = hit;
setHighlight(hit, true);
showInfo(hit.userData.info);
} else if (!e.target.closest('#info-panel') && !e.target.closest('.controls')) {
if (state.selected) setHighlight(state.selected, false);
state.selected = null;
infoPanel.classList.remove('visible');
}
});
function showInfo(info) {
infoTitle.textContent = info.name;
infoMeta.textContent = info.category || 'COMPONENT';
infoDesc.textContent = info.desc || '';
infoIcon.textContent = info.category === '外观' ? '⌚' : info.category === '显示' ? '👁️' : info.category === '调速' ? '⚖️' : info.category === '动力' ? '⚡' : '⚙️';
infoPanel.classList.add('visible');
}
document.getElementById('info-close').addEventListener('click', () => {
if (state.selected) setHighlight(state.selected, false);
state.selected = null;
infoPanel.classList.remove('visible');
});
// ===== Controls =====
const btnExplode = document.getElementById('btn-explode');
const btnAuto = document.getElementById('btn-auto');
const btnLume = document.getElementById('btn-lume');
const btnReset = document.getElementById('btn-reset');
btnExplode.addEventListener('click', () => {
state.exploded = !state.exploded;
btnExplode.classList.toggle('active', state.exploded);
btnExplode.textContent = state.exploded ? '🔧 组装视图' : '💥 爆炸视图';
});
btnAuto.addEventListener('click', () => {
state.autoRotate = !state.autoRotate;
controls.autoRotate = state.autoRotate;
btnAuto.classList.toggle('active', state.autoRotate);
});
btnLume.addEventListener('click', () => {
state.lumeOn = !state.lumeOn;
btnLume.classList.toggle('active', state.lumeOn);
mat.lume.emissiveIntensity = state.lumeOn ? CONFIG.lumeIntensity : 0.05;
mat.lumeDim.emissiveIntensity = state.lumeOn ? 0.15 : 0.01;
lumeLight.intensity = state.lumeOn ? 2 : 0.1;
});
btnReset.addEventListener('click', () => {
controls.reset();
camera.position.set(0, 0, 14);
watchGroup.rotation.set(-0.12, 0.08, 0);
});
// ===== Animation =====
const clock = new THREE.Clock();
function updateTime(now) {
const ms = now.getMilliseconds();
const s = now.getSeconds() + ms / 1000;
const m = now.getMinutes() + s / 60;
const h = (now.getHours() % 12) + m / 60;
// Smooth sweep hands
hands.second.rotation.z = -s * 6 * DEG;
hands.minute.rotation.z = -m * 6 * DEG;
hands.hour.rotation.z = -h * 30 * DEG;
// Gear train
const t = s * DEG;
gears.forEach(g => {
g.rotation.z = t * g.userData.rate;
});
}
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
const time = clock.getElapsedTime();
if (!state.paused) {
updateTime(new Date());
}
// Balance wheel oscillation ~4Hz
oscillators.forEach(o => {
o.group.rotation.z = Math.sin(time * 8 * Math.PI) * 0.45;
});
// Exploded view interpolation
const target = state.exploded ? 1 : 0;
if (Math.abs(state.explodeFactor - target) > 0.001) {
state.explodeFactor += (target - state.explodeFactor) * 4 * dt;
} else {
state.explodeFactor = target;
}
const ef = state.explodeFactor * CONFIG.explodeDistance;
explodeParts.forEach(p => {
if (!p.userData.homePos) return;
const dir = p.userData.explodeDir || new THREE.Vector3(0, 0, 1);
const mult = p.userData.explodeMult || 1;
p.position.copy(p.userData.homePos).add(dir.clone().multiplyScalar(ef * mult));
});
// Subtle watch floating
watchGroup.position.y = Math.sin(time * 0.6) * 0.08;
controls.update();
updateLabels();
composer.render();
}
// ===== Resize =====
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
bloomPass.setSize(window.innerWidth, window.innerHeight);
});
// ===== Init =====
document.getElementById('loading').classList.add('hidden');
animate();
</script>
</body>
</html>