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.
This commit is contained in:
OHCJ099 authored and GitHub committed 2026-06-21 11:19:25 +01:00
1 parent c9cf217db3
commit 75b18a76e4
16 files changed
+8031 -16

No files matched your search

+7 -7
View File
@@ -42,23 +42,23 @@ One-shot LLM eval cases by NAGI STUDIO: same prompt, different model x harness x
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
| GPT-5.3 Codex | OpenAI | Cursor · xhigh | 02 |
| GPT-5.2 | OpenAI | Cursor · xhigh | 02 |
| Gemini 3.1 Pro | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 05 |
| Gemini 3.5 Flash | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 05 |
| Gemini 3.1 Pro | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 06 |
| Gemini 3.5 Flash | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 06 |
| Grok Build | xAI | Grok Build TUI · Max | 02 |
| Nex-N2-Pro | Nex-AGI | Claude Code · Max | 02 |
| Grok 4.3 | xAI | Cursor · Default | 02 |
| Composer 2.5 | Cursor | Cursor · Max<br>Cursor · Default<br>Grok Build TUI · Default | 05 |
| Mistral Medium 3.5 | Mistral AI | Vibe · Thinking | 02 |
| DeepSeek-V4-Pro | DeepSeek | Claude Code · Max | 02 |
| DeepSeek-V4-Pro | DeepSeek | Claude Code · Max | 03 |
| Doubao Seed 2.0 Pro | ByteDance | Doubao Web · Pro Mode | 01 |
| Doubao Seed 2.0 Mini | ByteDance | Doubao Web · Fast Mode | 01 |
| MiMo v2.5 Pro | Xiaomi | Claude Code · Max | 02 |
| MiMo v2.5 Pro UltraSpeed | Xiaomi | Web · Default | 04 |
| Kimi K2.7-Code | Moonshot AI | Kimi Code · Thinking | 02 |
| MiMo v2.5 Pro | Xiaomi | Claude Code · Max<br>MiMo Code · Max | 03 |
| MiMo v2.5 Pro UltraSpeed | Xiaomi | Web · Default | 05 |
| Kimi K2.7-Code | Moonshot AI | Kimi Code · Thinking | 03 |
| Kimi K2.6 | Moonshot AI | Kimi Code · Thinking | 02 |
| MiniMax M3 | MiniMax | MiniMax Code Web · Thinking | 02 |
| MiniMax M2.7 | MiniMax | MiniMax Code Web · Thinking | 01 |
| GLM-5.2 | Zhipu AI | ZCode · Max | 03 |
| GLM-5.2 | Zhipu AI | ZCode · Max | 04 |
| GLM-5 Turbo | Zhipu AI | ZCode · Thinking | 03 |
| Qwen3.7-Max | Alibaba | Qoder · Default | 02 |
| Step 3.7 Flash | StepFun | Claude Code · High | 02 |
+7 -7
View File
@@ -42,23 +42,23 @@ NAGI STUDIO 的 LLM 测评案例集:同一段提示词,不同「模型 × Ha
| GPT-5.4 | OpenAI | Cursor · xhigh<br>Codex CLI · xhigh | 05 |
| GPT-5.3 Codex | OpenAI | Cursor · xhigh | 02 |
| GPT-5.2 | OpenAI | Cursor · xhigh | 02 |
| Gemini 3.1 Pro | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 05 |
| Gemini 3.5 Flash | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 05 |
| Gemini 3.1 Pro | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 06 |
| Gemini 3.5 Flash | Google | AntiGravity · High<br>Cursor · Default<br>Google AI Studio · High | 06 |
| Grok Build | xAI | Grok Build TUI · Max | 02 |
| Nex-N2-Pro | Nex-AGI | Claude Code · Max | 02 |
| Grok 4.3 | xAI | Cursor · Default | 02 |
| Composer 2.5 | Cursor | Cursor · Max<br>Cursor · Default<br>Grok Build TUI · Default | 05 |
| Mistral Medium 3.5 | Mistral AI | Vibe · Thinking | 02 |
| DeepSeek-V4-Pro | DeepSeek | Claude Code · Max | 02 |
| DeepSeek-V4-Pro | DeepSeek | Claude Code · Max | 03 |
| Doubao Seed 2.0 Pro | ByteDance | Doubao Web · Pro Mode | 01 |
| Doubao Seed 2.0 Mini | ByteDance | Doubao Web · Fast Mode | 01 |
| MiMo v2.5 Pro | Xiaomi | Claude Code · Max | 02 |
| MiMo v2.5 Pro UltraSpeed | Xiaomi | Web · Default | 04 |
| Kimi K2.7-Code | Moonshot AI | Kimi Code · Thinking | 02 |
| MiMo v2.5 Pro | Xiaomi | Claude Code · Max<br>MiMo Code · Max | 03 |
| MiMo v2.5 Pro UltraSpeed | Xiaomi | Web · Default | 05 |
| Kimi K2.7-Code | Moonshot AI | Kimi Code · Thinking | 03 |
| Kimi K2.6 | Moonshot AI | Kimi Code · Thinking | 02 |
| MiniMax M3 | MiniMax | MiniMax Code Web · Thinking | 02 |
| MiniMax M2.7 | MiniMax | MiniMax Code Web · Thinking | 01 |
| GLM-5.2 | Zhipu AI | ZCode · Max | 03 |
| GLM-5.2 | Zhipu AI | ZCode · Max | 04 |
| GLM-5 Turbo | Zhipu AI | ZCode · Thinking | 03 |
| Qwen3.7-Max | Alibaba | Qoder · Default | 02 |
| Step 3.7 Flash | StepFun | Claude Code · High | 02 |
+7
View File
@@ -19,6 +19,13 @@
"en": "Generated in one shot via Claude Code with DeepSeek V4 Pro (max thinking effort). Scene includes sky, ocean, beach, palm trees, seagulls, detailed bicycle (spokes, chain, gears), and a pelican (beak, gular pouch, wings, pedaling pose)."
},
"contributor": "Mintcolour"
},
"skeleton-watch": {
"note": {
"zh": "在 Claude Code 中以 DeepSeek-V4-Pro(最高思考强度)一次生成,未进行任何修改。",
"en": "Generated in one shot by DeepSeek-V4-Pro inside Claude Code with Max effort, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+7
View File
@@ -20,6 +20,13 @@
"en": "Contributed via community PR #3 (a Codex workflow); the submitter did not specify the runtime or effort level."
},
"contributor": "XA2333"
},
"skeleton-watch": {
"note": {
"zh": "通过 AntiGravity 生成(thinking effort: high);一次生成,未进行任何修改。",
"en": "Generated via AntiGravity (thinking effort: high); one shot, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+7
View File
@@ -20,6 +20,13 @@
"en": "Contributed via community PR #3 (a Codex workflow); the submitter did not specify the runtime or effort level."
},
"contributor": "XA2333"
},
"skeleton-watch": {
"note": {
"zh": "通过 AntiGravity 生成(thinking effort: high);一次生成,未进行任何修改。",
"en": "Generated via AntiGravity (thinking effort: high); one shot, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+8 -1
View File
@@ -29,6 +29,13 @@
},
"contributor": "Nagi-ovo"
}
]
],
"skeleton-watch": {
"note": {
"zh": "在 ZCode 中以 GLM-5.2(Max 思考配额)一次生成,未进行任何修改。",
"en": "Generated in one shot by GLM-5.2 (Max effort) inside ZCode, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+7
View File
@@ -19,6 +19,13 @@
"en": "Generated in one shot by Kimi K2.7-Code running in Kimi Code with Thinking mode."
},
"contributor": "OHCJ099"
},
"skeleton-watch": {
"note": {
"zh": "在 Kimi Code 中使用 Kimi K2.7-Code 模型(Thinking 档位)一次生成,未进行任何修改。",
"en": "Generated in one shot by Kimi K2.7-Code running in Kimi Code with Thinking mode, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"label": "MiMo v2.5 Pro",
"vendor": "Xiaomi",
"harness": "MiMo Code",
"effort": "Max",
"order": 97.2,
"runs": {
"skeleton-watch": {
"note": {
"zh": "在 MiMo Code 中使用 MiMo v2.5 Pro 模型一次生成,未进行任何修改。",
"en": "Generated in one shot by MiMo v2.5 Pro in MiMo Code, unmodified."
},
"contributor": "OHCJ099"
}
}
}
+8 -1
View File
@@ -38,6 +38,13 @@
"file": "pelican-cycling-2.svg",
"contributor": "OHCJ099"
}
]
],
"skeleton-watch": {
"note": {
"zh": "在 Web 环境中使用 MiMo v2.5 Pro UltraSpeed 一次生成,未做任何修改。",
"en": "Generated in one shot by MiMo v2.5 Pro UltraSpeed in Web environment, with no modifications."
},
"contributor": "OHCJ099"
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,595 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aero Tourbillon - 3D Skeleton Watch</title>
<style>
body { margin: 0; overflow: hidden; background: radial-gradient(circle at center, #1a1a2e 0%, #000000 100%); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: white; user-select: none; }
#canvas-container { width: 100vw; height: 100vh; cursor: grab; }
#canvas-container:active { cursor: grabbing; }
#ui-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
.panel { background: rgba(15, 15, 20, 0.85); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); border: 1px solid rgba(212, 175, 55, 0.3); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.5), inset 0 0 20px rgba(212, 175, 55, 0.05); padding: 25px; pointer-events: auto; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
#info-panel { position: absolute; bottom: 40px; left: 40px; width: 320px; transform: translateY(150%); opacity: 0; }
#info-panel.visible { transform: translateY(0); opacity: 1; }
#info-panel h2 { margin: 0 0 15px 0; font-size: 22px; color: #d4af37; text-transform: uppercase; letter-spacing: 2px; border-bottom: 1px solid rgba(212, 175, 55, 0.3); padding-bottom: 10px; }
#info-panel p { margin: 0; font-size: 15px; line-height: 1.6; color: #dddddd; }
#controls { position: absolute; top: 40px; right: 40px; display: flex; flex-direction: column; gap: 15px; }
button { background: linear-gradient(135deg, #d4af37 0%, #aa8c2c 100%); border: none; padding: 12px 24px; color: #000; font-weight: bold; border-radius: 6px; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: transform 0.2s, box-shadow 0.2s; pointer-events: auto; outline: none; }
button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(212, 175, 55, 0.4); }
button:active { transform: translateY(0); }
#tooltip { position: absolute; background: rgba(0,0,0,0.9); border: 1px solid #d4af37; padding: 8px 12px; border-radius: 4px; font-size: 13px; font-weight: bold; letter-spacing: 1px; color: #d4af37; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 10; white-space: nowrap; transform: translate(15px, 15px); }
#title { position: absolute; top: 40px; left: 40px; pointer-events: auto; }
#title h1 { margin: 0; font-size: 32px; font-weight: 300; letter-spacing: 6px; }
#title span { color: #d4af37; font-weight: 600; text-shadow: 0 0 15px rgba(212,175,55,0.4); }
#title p { margin-top:8px; color:#aaa; font-size: 12px; letter-spacing: 3px; margin-bottom: 0; }
#loading { position:fixed; top:0; left:0; width:100%; height:100%; background:#0a0a0e; z-index:9999; display:flex; flex-direction: column; justify-content:center; align-items:center; color:#d4af37; font-size:24px; letter-spacing:4px; transition: opacity 0.8s ease; pointer-events: none; }
.spinner { width: 50px; height: 50px; border: 3px solid rgba(212,175,55,0.3); border-radius: 50%; border-top-color: #d4af37; animation: spin 1s ease-in-out infinite; margin-bottom: 20px; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="loading">
<div class="spinner"></div>
<div>ASSEMBLING MOVEMENT...</div>
</div>
<div id="canvas-container"></div>
<div id="ui-container">
<div id="title">
<h1>AERO<span>TOURBILLON</span></h1>
<p>CALIBER X-01 SKELETON</p>
</div>
<div id="controls">
<button id="btn-explode">Toggle Exploded View</button>
<button id="btn-time">Pause/Resume Time</button>
</div>
<div id="info-panel" class="panel">
<h2 id="info-title">Component</h2>
<p id="info-desc">Description goes here.</p>
</div>
<div id="tooltip">Tooltip</div>
</div>
<!-- Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
<script>
let scene, camera, renderer, controls, raycaster;
const watchParts = [];
let interactableGroup;
const originalEmissives = new Map();
let hoveredPart = null;
let lockedPart = null;
const mouse = new THREE.Vector2();
let mouseX = 0, mouseY = 0;
let time = 0;
let timeFlowing = true;
let isExploded = false;
let dynamicData = {};
init();
animate();
function createEnvMap(renderer) {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#0a0a0a');
gradient.addColorStop(0.5, '#2a2a2a');
gradient.addColorStop(1, '#050505');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1024, 512);
ctx.fillStyle = '#ffffff';
ctx.fillRect(300, 50, 424, 150);
ctx.fillStyle = '#aaaaaa';
ctx.fillRect(50, 200, 100, 200);
ctx.fillRect(874, 200, 100, 200);
ctx.fillStyle = '#443322';
ctx.fillRect(400, 350, 224, 100);
const texture = new THREE.CanvasTexture(canvas);
texture.mapping = THREE.EquirectangularReflectionMapping;
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
texture.dispose();
pmremGenerator.dispose();
return envMap;
}
function init() {
const container = document.getElementById('canvas-container');
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(40, 50, 60);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap at 2x for performance
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
renderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxDistance = 150;
controls.minDistance = 20;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.8;
const envMap = createEnvMap(renderer);
scene.environment = envMap;
const spotLight = new THREE.SpotLight(0xffffff, 4);
spotLight.position.set(20, 60, 20);
spotLight.angle = Math.PI / 4;
spotLight.penumbra = 0.4;
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 2048;
spotLight.shadow.mapSize.height = 2048;
spotLight.shadow.bias = -0.0005;
scene.add(spotLight);
const rimLight = new THREE.PointLight(0x5588ff, 2, 100);
rimLight.position.set(-30, -10, -30);
scene.add(rimLight);
const warmLight = new THREE.PointLight(0xffaa55, 2, 100);
warmLight.position.set(30, -10, 30);
scene.add(warmLight);
const materials = {
gold: new THREE.MeshStandardMaterial({ color: 0xd4af37, metalness: 1.0, roughness: 0.15 }),
roseGold: new THREE.MeshStandardMaterial({ color: 0xb76e79, metalness: 1.0, roughness: 0.2 }),
silver: new THREE.MeshStandardMaterial({ color: 0xe8e8e8, metalness: 1.0, roughness: 0.1 }),
brushedMetal: new THREE.MeshStandardMaterial({ color: 0x999999, metalness: 0.8, roughness: 0.4 }),
darkMetal: new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.9, roughness: 0.3 }),
ruby: new THREE.MeshPhysicalMaterial({ color: 0xff0044, transmission: 0.9, opacity: 1, metalness: 0, roughness: 0, clearcoat: 1.0, clearcoatRoughness: 0.1, ior: 1.76 }),
glass: new THREE.MeshPhysicalMaterial({ color: 0xffffff, transmission: 1.0, opacity: 1, metalness: 0, roughness: 0, clearcoat: 1.0, ior: 1.5, transparent: true }),
lume: new THREE.MeshStandardMaterial({ color: 0xe0ffe0, emissive: 0x00ff44, emissiveIntensity: 1.5, roughness: 0.4 }),
screws: new THREE.MeshStandardMaterial({ color: 0x113388, metalness: 0.9, roughness: 0.2 })
};
function createGear(radius, teethCount, thickness, material, spokeCount = 5) {
const group = new THREE.Group();
if (teethCount > 0) {
const shape = new THREE.Shape();
const innerRadius = radius * 0.85;
for (let i = 0; i <= teethCount * 2; i++) {
const r = i % 2 === 0 ? radius : innerRadius;
const angle = (i / (teethCount * 2)) * Math.PI * 2;
if (i === 0) shape.moveTo(Math.cos(angle) * r, Math.sin(angle) * r);
else shape.lineTo(Math.cos(angle) * r, Math.sin(angle) * r);
}
const holePath = new THREE.Path();
holePath.absarc(0, 0, radius * 0.75, 0, Math.PI * 2, true);
shape.holes.push(holePath);
const extrudeSettings = { depth: thickness, bevelEnabled: true, bevelSegments: 1, steps: 1, bevelSize: thickness * 0.05, bevelThickness: thickness * 0.05 };
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
geometry.center();
const gearMesh = new THREE.Mesh(geometry, material);
gearMesh.rotation.x = Math.PI / 2;
group.add(gearMesh);
} else {
const ringGeom = new THREE.TorusGeometry(radius, thickness/2, 16, 64);
const ringMesh = new THREE.Mesh(ringGeom, material);
ringMesh.rotation.x = Math.PI / 2;
group.add(ringMesh);
}
if (spokeCount > 0) {
const centerGeom = new THREE.CylinderGeometry(radius * 0.15, radius * 0.15, thickness * 1.2, 16);
const centerMesh = new THREE.Mesh(centerGeom, material);
group.add(centerMesh);
for (let i = 0; i < spokeCount; i++) {
const spokeGeom = new THREE.BoxGeometry(radius * 0.1, thickness * 0.8, radius * 0.8);
const spoke = new THREE.Mesh(spokeGeom, material);
const angle = (i / spokeCount) * Math.PI * 2;
spoke.position.x = Math.cos(angle) * (radius * 0.4);
spoke.position.z = Math.sin(angle) * (radius * 0.4);
spoke.rotation.y = -angle;
group.add(spoke);
}
}
const axisGeom = new THREE.CylinderGeometry(radius * 0.05, radius * 0.05, thickness * 3, 16);
const axis = new THREE.Mesh(axisGeom, materials.silver);
group.add(axis);
return group;
}
function createBridge(points, holes, yPos, material) {
const shape = new THREE.Shape();
points.forEach((p, i) => {
if(i===0) shape.moveTo(p[0], p[1]);
else shape.lineTo(p[0], p[1]);
});
holes.forEach(h => {
const holePath = new THREE.Path();
holePath.absarc(h[0], h[1], h[2], 0, Math.PI * 2, true);
shape.holes.push(holePath);
});
const extrudeSettings = { depth: 0.5, bevelEnabled: true, bevelSegments: 3, steps: 1, bevelSize: 0.1, bevelThickness: 0.1 };
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI / 2;
mesh.position.y = yPos;
const group = new THREE.Group();
group.add(mesh);
holes.forEach(h => {
const rubyGeom = new THREE.CylinderGeometry(h[2]*1.2, h[2]*1.2, 0.6, 16);
const ruby = new THREE.Mesh(rubyGeom, materials.ruby);
ruby.position.set(h[0], yPos, -h[1]);
group.add(ruby);
const screwGeom = new THREE.CylinderGeometry(0.3, 0.3, 0.4, 16);
const screw = new THREE.Mesh(screwGeom, materials.screws);
screw.position.set(h[0] + h[2]*1.8, yPos, -h[1] + h[2]*1.8);
group.add(screw);
});
return group;
}
function makeHand(width, length, tail, color, hasLume) {
const shape = new THREE.Shape();
shape.moveTo(-width/2, -tail);
shape.lineTo(width/2, -tail);
shape.lineTo(width*0.6/2, length*0.9);
shape.lineTo(0, length);
shape.lineTo(-width*0.6/2, length*0.9);
const geom = new THREE.ExtrudeGeometry(shape, { depth: 0.1, bevelEnabled: true, bevelSize: 0.02, bevelThickness: 0.02 });
const mesh = new THREE.Mesh(geom, color);
mesh.rotation.x = Math.PI / 2;
const hGroup = new THREE.Group();
hGroup.add(mesh);
if(hasLume) {
const lumeShape = new THREE.Shape();
lumeShape.moveTo(-width*0.3, 2);
lumeShape.lineTo(width*0.3, 2);
lumeShape.lineTo(width*0.2, length*0.8);
lumeShape.lineTo(-width*0.2, length*0.8);
const lumeGeom = new THREE.ExtrudeGeometry(lumeShape, { depth: 0.12, bevelEnabled: false });
const lumeMesh = new THREE.Mesh(lumeGeom, materials.lume);
lumeMesh.rotation.x = Math.PI / 2;
hGroup.add(lumeMesh);
}
return hGroup;
}
function createHands() {
const group = new THREE.Group();
const minHand = makeHand(1.0, 15, 2, materials.silver, true);
minHand.position.y = 0.5;
group.add(minHand);
const hourHand = makeHand(1.4, 10, 1.5, materials.silver, true);
hourHand.position.y = 0.2;
group.add(hourHand);
const secHand = makeHand(0.4, 16, 3, materials.ruby, false);
secHand.position.y = 0.8;
group.add(secHand);
const capGeom = new THREE.CylinderGeometry(0.8, 0.8, 0.5, 32);
const cap = new THREE.Mesh(capGeom, materials.silver);
cap.position.y = 1.0;
group.add(cap);
return { group, hourHand, minHand, secHand };
}
function createDial() {
const group = new THREE.Group();
const ringGeom = new THREE.RingGeometry(16, 17.5, 64);
const ring = new THREE.Mesh(ringGeom, materials.darkMetal);
ring.rotation.x = -Math.PI / 2;
group.add(ring);
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const isQuarter = i % 3 === 0;
const width = isQuarter ? 0.6 : 0.4;
const length = isQuarter ? 2.5 : 1.5;
const indexGeom = new THREE.BoxGeometry(width, 0.4, length);
const indexMesh = new THREE.Mesh(indexGeom, materials.silver);
const lumeGeom = new THREE.BoxGeometry(width * 0.6, 0.45, length * 0.8);
const lumeMesh = new THREE.Mesh(lumeGeom, materials.lume);
const indexGroup = new THREE.Group();
indexGroup.add(indexMesh);
indexGroup.add(lumeMesh);
indexGroup.position.set(Math.cos(angle) * 16, 0.2, Math.sin(angle) * 16);
indexGroup.rotation.y = -angle;
group.add(indexGroup);
}
return group;
}
function createTourbillon(x, y, z) {
const group = new THREE.Group();
group.position.set(x, y, z);
const cageGeom = new THREE.TorusGeometry(3.5, 0.2, 16, 32);
const cage = new THREE.Mesh(cageGeom, materials.silver);
cage.rotation.x = Math.PI / 2;
group.add(cage);
for(let i=0; i<3; i++) {
const supportGeom = new THREE.BoxGeometry(0.3, 1.5, 3.5);
const support = new THREE.Mesh(supportGeom, materials.silver);
const angle = (i/3) * Math.PI * 2;
support.position.set(Math.cos(angle)*1.75, 0.75, Math.sin(angle)*1.75);
support.rotation.y = -angle;
cage.add(support);
}
const balanceWheel = createGear(2.5, 0, 0.2, materials.gold, 3);
balanceWheel.position.y = 0.5;
for(let i=0; i<12; i++) {
const screwGeom = new THREE.CylinderGeometry(0.15, 0.15, 0.3, 8);
const screw = new THREE.Mesh(screwGeom, materials.screws);
const angle = (i/12) * Math.PI * 2;
screw.position.set(Math.cos(angle)*2.5, 0, Math.sin(angle)*2.5);
screw.rotation.x = Math.PI / 2;
screw.rotation.z = angle;
balanceWheel.add(screw);
}
const curve = new THREE.Curve();
curve.getPoint = function(t) {
const angle = t * Math.PI * 10;
const radius = 0.2 + t * 1.5;
return new THREE.Vector3(Math.cos(angle)*radius, 0, Math.sin(angle)*radius);
};
const springGeom = new THREE.TubeGeometry(curve, 100, 0.02, 8, false);
const spring = new THREE.Mesh(springGeom, materials.silver);
spring.position.y = 0.8;
balanceWheel.add(spring);
cage.add(balanceWheel);
const escapeWheel = createGear(1, 15, 0.1, materials.silver, 4);
escapeWheel.position.set(2.5, 0.5, -2.5);
cage.add(escapeWheel);
return { group, cage, balanceWheel, escapeWheel };
}
function createBarrel(radius, thickness) {
const group = new THREE.Group();
const barrelGear = createGear(radius, 72, thickness, materials.gold, 0);
group.add(barrelGear);
const coverGeom = new THREE.CylinderGeometry(radius * 0.95, radius * 0.95, thickness * 1.1, 32);
const cover = new THREE.Mesh(coverGeom, materials.darkMetal);
group.add(cover);
return group;
}
interactableGroup = new THREE.Group();
scene.add(interactableGroup);
function addPart(mesh, name, desc, expY) {
mesh.userData.parentPart = { name, desc, object: mesh, origY: mesh.position.y, expY };
mesh.traverse(c => {
if (c.isMesh) {
c.userData.parentPart = mesh.userData.parentPart;
c.castShadow = true;
c.receiveShadow = true;
}
});
watchParts.push(mesh.userData.parentPart);
interactableGroup.add(mesh);
}
// Assembly
const topGlass = new THREE.Mesh(new THREE.CylinderGeometry(18.5, 18.5, 0.2, 64), materials.glass);
topGlass.position.y = 4.5;
topGlass.castShadow = false;
addPart(topGlass, "Sapphire Crystal", "Scratch-resistant synthetic sapphire with double anti-reflective coating.", 25);
const bezel = new THREE.Mesh(new THREE.TorusGeometry(19, 1.2, 32, 64), materials.silver);
bezel.rotation.x = Math.PI / 2;
bezel.position.y = 2.5;
for(let i=0; i<8; i++) {
const screw = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 0.5, 16), materials.darkMetal);
const angle = (i/8)*Math.PI*2;
screw.position.set(Math.cos(angle)*19, 0.5, Math.sin(angle)*19);
bezel.add(screw);
}
addPart(bezel, "Polished Bezel", "Hand-polished stainless steel bezel securing the crystal to the main case.", 18);
const handsData = createHands();
handsData.group.position.y = 3.5;
addPart(handsData.group, "Luminous Hands", "Diamond-cut facetted hands applied with high-intensity Super-LumiNova.", 12);
const dial = createDial();
dial.position.y = 2.0;
addPart(dial, "Skeleton Dial", "Floating indices ring providing clear time reading without obstructing the mechanical view.", 7);
const bridge1Points = [[-5, 9], [5, 9], [7, -5], [2, -6], [-3, -2]];
const bridge1Holes = [[0, 6.5, 0.8], [0, 0, 0.5], [4.5, -3, 0.5]];
const topBridge = createBridge(bridge1Points, bridge1Holes, 1.2, materials.darkMetal);
const tourbBridgePoints = [[-3, -5], [3, -5], [2, -9], [-2, -9]];
const tourbBridgeHoles = [[0, -7, 0.5]];
const tourbBridge = createBridge(tourbBridgePoints, tourbBridgeHoles, 1.2, materials.silver);
const topBridgeGroup = new THREE.Group();
topBridgeGroup.add(topBridge, tourbBridge);
addPart(topBridgeGroup, "Upper Bridges", "Micro-blasted titanium bridges holding the gear train in perfect alignment.", 3);
const gearTrain = new THREE.Group();
const centerWheel = createGear(4, 60, 0.4, materials.gold, 5);
centerWheel.position.set(0, -0.5, 0);
const thirdWheel = createGear(3, 45, 0.4, materials.roseGold, 5);
thirdWheel.position.set(4.5, 0.5, 3);
gearTrain.add(centerWheel, thirdWheel);
addPart(gearTrain, "Gear Train", "High-precision brass and rose gold wheels transmitting power smoothly from the barrel.", 0);
const tourbillonData = createTourbillon(0, 0, 7);
addPart(tourbillonData.group, "Flying Tourbillon", "One-minute tourbillon countering the effects of gravity on the escapement for ultimate precision.", 0);
const barrel = createBarrel(5.5, 1.2);
barrel.position.set(0, 0, -6.5);
addPart(barrel, "Mainspring Barrel", "Houses the massive mainspring, providing a robust 72-hour power reserve.", 0);
const bpShape = new THREE.Shape();
bpShape.absarc(0, 0, 18, 0, Math.PI * 2, false);
const bpCutouts = [[0, 6.5, 5], [0, 0, 3.5], [4.5, -3, 2.5], [0, -7, 4.5], [-5, -2, 3], [-4, 3, 2.5]];
bpCutouts.forEach(c => {
const path = new THREE.Path();
path.absarc(c[0], c[1], c[2], 0, Math.PI * 2, true);
bpShape.holes.push(path);
});
const bpGeom = new THREE.ExtrudeGeometry(bpShape, { depth: 0.8, bevelEnabled: true, bevelSegments: 2, steps: 1, bevelSize: 0.2, bevelThickness: 0.2 });
const basePlate = new THREE.Mesh(bpGeom, materials.darkMetal);
basePlate.rotation.x = Math.PI / 2;
basePlate.position.y = -1.5;
addPart(basePlate, "Main Plate", "The structural foundation of the movement, intricately skeletonized to reduce weight.", -5);
const mainCase = new THREE.Mesh(new THREE.CylinderGeometry(19.5, 19, 5, 64), materials.brushedMetal);
mainCase.position.y = -0.5;
const lugGeom = new THREE.BoxGeometry(4, 3, 8);
const positions = [ [12, 0, 18], [-12, 0, 18], [12, 0, -18], [-12, 0, -18] ];
positions.forEach(pos => {
const lug = new THREE.Mesh(lugGeom, materials.silver);
lug.position.set(...pos);
lug.lookAt(0,0,0);
mainCase.add(lug);
});
const crown = new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 1.5, 32), materials.gold);
crown.rotation.z = Math.PI / 2;
crown.position.set(20.5, 0, 0);
mainCase.add(crown);
addPart(mainCase, "Oyster Case", "40mm brushed steel case ensuring water resistance to 100 meters.", -12);
const rotorShape = new THREE.Shape();
rotorShape.absarc(0, 0, 16, 0, Math.PI, false);
rotorShape.absarc(0, 0, 4, Math.PI, 0, true);
const rotorGeom = new THREE.ExtrudeGeometry(rotorShape, { depth: 0.5, bevelEnabled: true, bevelSize: 0.1, bevelThickness: 0.1 });
const rotorGroup = new THREE.Group();
const rotorMesh = new THREE.Mesh(rotorGeom, materials.roseGold);
rotorMesh.rotation.x = Math.PI / 2;
rotorGroup.add(rotorMesh);
rotorGroup.position.y = -2.8;
addPart(rotorGroup, "Winding Rotor", "Heavy tungsten oscillating weight that winds the mainspring automatically with wrist movement.", -16);
const bottomGlass = new THREE.Mesh(new THREE.CylinderGeometry(18.5, 18.5, 0.2, 64), materials.glass);
bottomGlass.position.y = -3.5;
bottomGlass.castShadow = false;
addPart(bottomGlass, "Exhibition Caseback", "Sapphire crystal allowing full view of the movement's spectacular rear architecture.", -20);
dynamicData = {
balanceWheel: tourbillonData.balanceWheel,
escapeWheel: tourbillonData.escapeWheel,
cage: tourbillonData.cage,
secHand: handsData.secHand,
minHand: handsData.minHand,
hourHand: handsData.hourHand,
centerWheel: centerWheel,
thirdWheel: thirdWheel,
barrel: barrel,
rotor: rotorGroup
};
raycaster = new THREE.Raycaster();
window.addEventListener('mousemove', (event) => {
mouseX = event.clientX;
mouseY = event.clientY;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('click', (event) => {
if (event.target.tagName === 'BUTTON') return;
if (hoveredPart) {
if (lockedPart === hoveredPart) lockedPart = null;
else lockedPart = hoveredPart;
} else {
lockedPart = null;
}
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
document.getElementById('btn-explode').addEventListener('click', () => {
isExploded = !isExploded;
watchParts.forEach(part => {
const targetY = isExploded ? part.expY : part.origY;
new TWEEN.Tween(part.object.position)
.to({ y: targetY }, 1500)
.easing(TWEEN.Easing.Cubic.InOut)
.start();
});
});
document.getElementById('btn-time').addEventListener('click', () => {
timeFlowing = !timeFlowing;
});
setTimeout(() => {
const loader = document.getElementById('loading');
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 800);
}, 1500);
}
function highlight(part) {
if (hoveredPart === part) return;
if (hoveredPart && hoveredPart !== lockedPart) unhighlightPart(hoveredPart);
hoveredPart = part;
part.object.traverse(child => {
if (child.isMesh && child.material) {
if(!originalEmissives.has(child)) {
originalEmissives.set(child, child.material.emissive ? child.material.emissive.clone() : new THREE.Color(0x000000));
}
if (!child.material.emissive) child.material.emissive = new THREE.Color();
child.material.emissive.setHex(0x444444);
}
});
document.getElementById('info-title').innerText = part.name;
document.getElementById('info-desc').innerText = part.desc;
document.getElementById('info-panel').classList.add('visible');
}
function unhighlightPart(part) {
part.object.traverse(child => {
if (child.isMesh && child.material) {
const orig = originalEmissives.get(child);
if (orig) child.material.emissive.copy(orig);
}
});
if(hoveredPart === part) {
hoveredPart = null;
if(!lockedPart) document.getElementById('info-panel').classList.remove('visible');
}
}
function animate() {
requestAnimationFrame(animate);
TWEEN.update();
controls.update();
if (timeFlowing) {
time += 0.016;
dynamicData.balanceWheel.rotation.y = Math.sin(time * 30) * 1.5;
const tickStep = Math.floor(time * 5);
dynamicData.escapeWheel.rotation.y = tickStep * (Math.PI * 2 / 15);
dynamicData.cage.rotation.y = time * 0.5;
dynamicData.secHand.rotation.y = -time * 1;
dynamicData.minHand.rotation.y = -time * (1/60);
dynamicData.hourHand.rotation.y = -time * (1/3600);
dynamicData.centerWheel.rotation.y = time * 1;
dynamicData.thirdWheel.rotation.y = -time * 1.5;
dynamicData.barrel.rotation.y = time * 0.1;
dynamicData.rotor.rotation.z = Math.sin(time * 0.5) * 1.5 + Math.cos(time * 1.2) * 0.5;
}
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(interactableGroup.children, true);
if (intersects.length > 0) {
const part = intersects[0].object.userData.parentPart;
if (part) {
if(!lockedPart || lockedPart === part) {
highlight(part);
}
const tooltip = document.getElementById('tooltip');
tooltip.style.opacity = 1;
tooltip.innerText = part.name;
tooltip.style.left = (mouseX + 15) + 'px';
tooltip.style.top = (mouseY + 15) + 'px';
controls.autoRotate = false;
}
} else {
if (hoveredPart && hoveredPart !== lockedPart) unhighlightPart(hoveredPart);
document.getElementById('tooltip').style.opacity = 0;
if(!lockedPart && !isExploded) controls.autoRotate = true;
}
if(lockedPart) {
document.getElementById('info-title').innerText = lockedPart.name;
document.getElementById('info-desc').innerText = lockedPart.desc;
document.getElementById('info-panel').classList.add('visible');
}
renderer.render(scene, camera);
}
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,456 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calibre · Skeleton Haute Horlogerie</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#060606;overflow:hidden;font-family:'Cormorant Garamond',Georgia,serif;color:#c4b590;-webkit-font-smoothing:antialiased}
canvas{display:block}
.hdr{position:fixed;top:0;left:0;right:0;padding:28px 36px;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none;z-index:10}
.hdr>*{pointer-events:auto}
.brand h1{font-size:20px;font-weight:300;letter-spacing:10px;text-transform:uppercase;color:#c4b590}
.brand p{font-size:10px;font-family:'DM Mono',monospace;letter-spacing:4px;color:#6a6255;margin-top:3px;text-transform:uppercase}
.specs{text-align:right;font-family:'DM Mono',monospace;font-size:9px;letter-spacing:2px;color:#55504a;line-height:2.1;text-transform:uppercase}
.ctrl{position:fixed;bottom:28px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:10}
.btn{background:rgba(196,181,144,.05);border:1px solid rgba(196,181,144,.12);color:#9a9080;padding:9px 22px;font-family:'DM Mono',monospace;font-size:9px;letter-spacing:3px;text-transform:uppercase;cursor:pointer;transition:all .4s cubic-bezier(.16,1,.3,1);backdrop-filter:blur(12px);outline:none;border-radius:0}
.btn:hover{background:rgba(196,181,144,.12);border-color:rgba(196,181,144,.35);color:#c4b590}
.btn.on{background:rgba(196,181,144,.14);border-color:#c4b590;color:#c4b590}
.ip{position:fixed;top:50%;right:28px;transform:translateY(-50%) translateX(20px);width:250px;background:rgba(10,10,8,.92);border:1px solid rgba(196,181,144,.1);backdrop-filter:blur(24px);padding:26px;opacity:0;pointer-events:none;transition:all .5s cubic-bezier(.16,1,.3,1);z-index:10}
.ip.show{opacity:1;pointer-events:auto;transform:translateY(-50%) translateX(0)}
.ip h2{font-size:15px;font-weight:400;letter-spacing:4px;text-transform:uppercase;color:#c4b590;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid rgba(196,181,144,.12)}
.ip .d{font-family:'DM Mono',monospace;font-size:10px;line-height:2;color:#7a7265;letter-spacing:.5px}
.lc{position:fixed;inset:0;pointer-events:none;z-index:5}
.lb{position:absolute;font-family:'DM Mono',monospace;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:rgba(196,181,144,.65);background:rgba(6,6,6,.55);padding:3px 9px;border-left:1px solid rgba(196,181,144,.35);white-space:nowrap;opacity:0;transition:opacity .3s;transform:translate(-50%,-50%)}
.lb.v{opacity:1}
.ld{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#060606;z-index:100;transition:opacity 1s ease}
.ld.h{opacity:0;pointer-events:none}
.ld span{font-size:13px;letter-spacing:8px;text-transform:uppercase;color:#5a5548;animation:p 1.5s ease-in-out infinite}
@keyframes p{0%,100%{opacity:.3}50%{opacity:1}}
@media(max-width:768px){.hdr{padding:16px}.specs{display:none}.ctrl{flex-wrap:wrap;justify-content:center}.ip{width:200px;padding:18px;right:10px}}
</style>
</head>
<body>
<div class="ld" id="ld"><span>Calibrating</span></div>
<div class="hdr">
<div class="brand"><h1>Calibre</h1><p>Skeleton Collection &mdash; Ref. 7721</p></div>
<div class="specs"><div>41mm &middot; 18K Rose Gold</div><div>Hand-Wound &middot; 72h Power Reserve</div><div>28,800 vph &middot; 21 Jewels</div></div>
</div>
<div class="ctrl">
<button class="btn" id="bE" onclick="UI.explode()">Exploded View</button>
<button class="btn on" id="bR" onclick="UI.rotate()">Auto Rotate</button>
<button class="btn" id="bL" onclick="UI.labels()">Labels</button>
</div>
<div class="ip" id="iP"><h2 id="iT">Component</h2><div class="d" id="iD"></div></div>
<div class="lc" id="lC"></div>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.163.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';
/* ═══════════════════════ RENDERER ═══════════════════════ */
const R=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});
R.setSize(innerWidth,innerHeight);
R.setPixelRatio(Math.min(devicePixelRatio,2));
R.shadowMap.enabled=true;
R.shadowMap.type=THREE.PCFSoftShadowMap;
R.toneMapping=THREE.ACESFilmicToneMapping;
R.toneMappingExposure=1.15;
R.outputColorSpace=THREE.SRGBColorSpace;
document.body.appendChild(R.domElement);
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x060606);
const cam=new THREE.PerspectiveCamera(32,innerWidth/innerHeight,.1,100);
cam.position.set(2.5,4.5,5.5);
const ctl=new OrbitControls(cam,R.domElement);
ctl.enableDamping=true;ctl.dampingFactor=.06;
ctl.minDistance=3;ctl.maxDistance=14;
ctl.autoRotate=true;ctl.autoRotateSpeed=.6;
ctl.target.set(0,-.1,0);
/* env */
const pmrem=new THREE.PMREMGenerator(R);
scene.environment=pmrem.fromScene(new RoomEnvironment(),.04).texture;
/* lights */
const L1=new THREE.DirectionalLight(0xfff0d6,3.5);L1.position.set(5,8,4);L1.castShadow=true;
L1.shadow.mapSize.set(2048,2048);L1.shadow.bias=-.0005;scene.add(L1);
const L2=new THREE.DirectionalLight(0xd0e0ff,.7);L2.position.set(-6,3,-2);scene.add(L2);
const L3=new THREE.DirectionalLight(0xffeedd,2);L3.position.set(0,-3,-5);scene.add(L3);
scene.add(new THREE.PointLight(0xfff5e6,2,12,1.5).translateOnAxis(new THREE.Vector3(0,1,0),6));
scene.add(new THREE.AmbientLight(0x1a1a2e,.35));
/* ═══════════════════════ MATERIALS ═══════════════════════ */
const M={
steel:c({color:0xbbbbbb,metalness:.95,roughness:.28}),
polish:c({color:0xcccccc,metalness:1,roughness:.08,envMapIntensity:1.3}),
gold:c({color:0xd4a574,metalness:1,roughness:.18}),
blue:c({color:0x4455bb,metalness:.92,roughness:.12}),
ruby:new THREE.MeshPhysicalMaterial({color:0xdd2244,roughness:.05,transmission:.55,thickness:.3,ior:1.76,clearcoat:.5}),
crystal:new THREE.MeshPhysicalMaterial({color:0xffffff,roughness:0,transmission:.95,thickness:.4,ior:1.77,clearcoat:1,transparent:true,opacity:.35}),
lume:new THREE.MeshPhysicalMaterial({color:0x88ff99,emissive:0x44cc66,emissiveIntensity:1.8,roughness:.6}),
plate:c({color:0x7a7a7a,metalness:.92,roughness:.35,envMapIntensity:.8}),
bridge:c({color:0x8e8e8e,metalness:.95,roughness:.25}),
};
function c(o){return new THREE.MeshPhysicalMaterial(Object.assign({envMapIntensity:1},o))}
/* ═══════════════════════ GEOMETRY HELPERS ═══════════════════════ */
function circPath(cx,cy,r,n=40){
const p=new THREE.Path();
for(let i=0;i<=n;i++){const a=i/n*Math.PI*2;const x=cx+Math.cos(a)*r,y=cy+Math.sin(a)*r;i===0?p.moveTo(x,y):p.lineTo(x,y)}
return p;
}
function gearShape(teeth,outerR,rootR,holeR){
const s=new THREE.Shape(),st=Math.PI*2/teeth;
for(let i=0;i<teeth;i++){
const a=i*st,pts=[[0,rootR],[.22,rootR],[.28,outerR],[.72,outerR],[.78,rootR],[1,rootR]];
pts.forEach(([t,r],j)=>{
const ang=a+t*st,x=Math.cos(ang)*r,y=Math.sin(ang)*r;
(i===0&&j===0)?s.moveTo(x,y):s.lineTo(x,y);
});
}
s.closePath();
if(holeR>0)s.holes.push(circPath(0,0,holeR));
return s;
}
function mkGear(teeth,oR,rR,hR,th,mat,spokes){
const s=gearShape(teeth,oR,rR,hR);
if(spokes){
const{count:n,radius:r,distance:d}=spokes;
for(let i=0;i<n;i++){const a=i/n*Math.PI*2;s.holes.push(circPath(Math.cos(a)*d,Math.sin(a)*d,r,24));}
}
const g=new THREE.ExtrudeGeometry(s,{depth:th,bevelEnabled:true,bevelThickness:.004,bevelSize:.004,bevelSegments:1});
g.translate(0,0,-th/2);
const m=new THREE.Mesh(g,mat);m.castShadow=true;m.receiveShadow=true;return m;
}
function mkEscape(teeth,oR,iR,th,hR,mat){
const s=new THREE.Shape(),st=Math.PI*2/teeth;
for(let i=0;i<teeth;i++){
const a=i*st;
const pts=[[.05,iR],[.18,oR],[.22,iR],[.55,iR*.98],[.58,iR]];
pts.forEach(([t,r],j)=>{
const ang=a+t*st,x=Math.cos(ang)*r,y=Math.sin(ang)*r;
(i===0&&j===0)?s.moveTo(x,y):s.lineTo(x,y);
});
}
s.closePath();
if(hR>0)s.holes.push(circPath(0,0,hR));
const g=new THREE.ExtrudeGeometry(s,{depth:th,bevelEnabled:false});
g.translate(0,0,-th/2);
return new THREE.Mesh(g,mat);
}
function mkHairspring(iR,oR,turns,wR,mat){
class SC extends THREE.Curve{getPoint(t){const a=t*turns*Math.PI*2,r=iR+(oR-iR)*t;return new THREE.Vector3(Math.cos(a)*r,Math.sin(a)*r,0)}}
return new THREE.Mesh(new THREE.TubeGeometry(new SC(),turns*36,wR,4,false),mat);
}
function mkScrew(r){
const g=new THREE.Group();
const h=new THREE.Mesh(new THREE.CylinderGeometry(r,r,r*.35,6).rotateX(Math.PI/2),M.blue);g.add(h);
const sl=new THREE.Mesh(new THREE.BoxGeometry(r*1.7,r*.04,r*.14),M.steel);sl.position.z=r*.17;g.add(sl);
return g;
}
/* ═══════════════════════ WATCH PARTS ═══════════════════════ */
const watch=new THREE.Group();
const reg={},anim={},expP=[],labParts=[];
let exploded=false,showL=false;
function regPart(mesh,name,desc,eOff){
mesh.userData={name,desc,oZ:mesh.position.z,eOff:eOff||0};
reg[mesh.uuid]=mesh;
if(eOff!==undefined)expP.push(mesh);
labParts.push(mesh);
return mesh;
}
/* ── case body ── */
function buildCase(){
const g=new THREE.Group();
const pts=[new THREE.Vector2(1.7,-.48),new THREE.Vector2(2.02,-.48),new THREE.Vector2(2.07,-.38),
new THREE.Vector2(2.07,.34),new THREE.Vector2(2.02,.44),new THREE.Vector2(1.86,.5),new THREE.Vector2(1.74,.44),new THREE.Vector2(1.7,.34)];
const cm=new THREE.Mesh(new THREE.LatheGeometry(pts,64),M.gold);cm.castShadow=true;cm.receiveShadow=true;g.add(cm);
[[.28,1],[.72,1],[1.28,-1],[1.72,-1]].forEach(([a])=>{
const lg=new THREE.Mesh(new THREE.BoxGeometry(.28,.85,.28),M.gold);
lg.position.set(Math.cos(a*Math.PI)*2.14,Math.sin(a*Math.PI)*2.14,0);lg.rotation.z=a*Math.PI+Math.PI/2;lg.castShadow=true;g.add(lg);
});
const cw=new THREE.Mesh(new THREE.CylinderGeometry(.14,.16,.38,16).rotateZ(Math.PI/2),M.gold);cw.position.set(2.24,0,0);cw.castShadow=true;g.add(cw);
regPart(g,'表壳','18K 玫金表壳,41mm,抛光与拉丝双工艺处理,50米防水。',-2.8);
return g;
}
/* ── crystal ── */
function buildCrystal(){
const m=new THREE.Mesh(new THREE.SphereGeometry(1.78,64,32,0,Math.PI*2,0,Math.PI*.24),M.crystal);
m.position.z=.46;m.renderOrder=999;
regPart(m,'蓝宝石表镜','双面防反射涂层蓝宝石水晶,莫氏硬度9,透光率>99%。',1.8);
return m;
}
/* ── caseback ── */
function buildBack(){
const g=new THREE.Group();
const b=new THREE.Mesh(new THREE.CylinderGeometry(1.74,1.74,.06,64).rotateX(Math.PI/2),M.crystal);b.position.z=-.5;b.renderOrder=998;g.add(b);
g.add(new THREE.Mesh(new THREE.TorusGeometry(1.84,.055,8,64),M.gold).translateZ(-.5));
regPart(g,'透底表背','蓝宝石透底,完整展现机芯运转与手工修饰。',-1.8);
return g;
}
/* ── main plate ── */
function buildPlate(){
const s=new THREE.Shape();s.absarc(0,0,1.65,0,Math.PI*2,false);
[[0,0,.34],[-.5,.5,.28],[.5,.45,.24],[-.5,-.45,.28],[.5,-.5,.24],[0,.8,.18],[0,-.8,.18],[.8,0,.18],[-.8,0,.18],[-.3,-.7,.14],[.3,.7,.14]].forEach(([x,y,r])=>s.holes.push(circPath(x,y,r,28)));
const g=new THREE.ExtrudeGeometry(s,{depth:.07,bevelEnabled:true,bevelThickness:.004,bevelSize:.004,bevelSegments:1});g.translate(0,0,-.035);
const m=new THREE.Mesh(g,M.plate);m.position.z=-.1;m.castShadow=true;m.receiveShadow=true;
regPart(m,'主机板','镂空主机板,日内瓦波纹与鱼鳞纹手工装饰,大幅减重同时保证结构强度。',0);
return m;
}
/* ── barrel ── */
function buildBarrel(){
const g=new THREE.Group();
g.add(mkGear(38,.74,.64,.15,.14,M.gold,{count:5,radius:.14,distance:.41}));
g.add(new THREE.Mesh(new THREE.CylinderGeometry(.07,.07,.22,16).rotateX(Math.PI/2),M.polish));
const sp=mkHairspring(.08,.54,5,.007,M.blue);sp.position.z=.02;g.add(sp);
g.position.set(-.4,.35,-.05);
anim.barrel=g;
regPart(g,'发条盒','主发条提供72小时动力储备,手动上链,配备滑动发条防止过载。',-.6);
return g;
}
/* ── center wheel ── */
function buildCenter(){
const g=new THREE.Group();
g.add(mkGear(22,.48,.4,.07,.1,M.polish,{count:4,radius:.09,distance:.25}));
const pn=mkGear(10,.13,.09,.04,.12,M.polish);pn.position.z=.06;g.add(pn);
g.position.set(0,0,-.02);
anim.center=g;
regPart(g,'中心轮','中心轮驱动分针,每60分钟旋转一圈,与走时轮系直接啮合。',-.3);
return g;
}
/* ── third wheel ── */
function buildThird(){
const g=new THREE.Group();
g.add(mkGear(18,.36,.3,.05,.08,M.steel,{count:3,radius:.07,distance:.17}));
g.position.set(-.62,.48,.02);
anim.third=g;
regPart(g,'第三轮','过轮传递中心轮至第四轮的动力,调节转速比。',.15);
return g;
}
/* ── fourth wheel ── */
function buildFourth(){
const g=new THREE.Group();
g.add(mkGear(15,.26,.21,.04,.065,M.polish,{count:3,radius:.05,distance:.12}));
g.position.set(.28,-.52,.05);
anim.fourth=g;
regPart(g,'第四轮','秒轮,每分钟旋转一圈,轴心延伸至表盘面承载秒针。',.4);
return g;
}
/* ── escape wheel ── */
function buildEscape(){
const g=new THREE.Group();
g.add(mkEscape(15,.21,.11,.035,.025,M.polish));
g.position.set(-.42,-.38,.08);
anim.escape=g;
regPart(g,'擒纵轮','特殊齿形擒纵轮,与擒纵叉配合精确控制能量释放频率。',.65);
return g;
}
/* ── pallet fork ── */
function buildPallet(){
const g=new THREE.Group();
const fs=new THREE.Shape();
fs.moveTo(0,-.11);fs.lineTo(.018,.07);fs.lineTo(.075,.14);fs.lineTo(.055,.16);fs.lineTo(0,.09);fs.lineTo(-.055,.16);fs.lineTo(-.075,.14);fs.lineTo(-.018,.07);fs.closePath();
const fg=new THREE.ExtrudeGeometry(fs,{depth:.035,bevelEnabled:false});fg.translate(0,0,-.0175);
g.add(new THREE.Mesh(fg,M.steel));
[.065,-.065].forEach(x=>{const s=new THREE.Mesh(new THREE.BoxGeometry(.018,.055,.035),M.ruby);s.position.set(x,.15,0);g.add(s);});
g.position.set(-.42,-.55,.1);
anim.pallet=g;
regPart(g,'擒纵叉','双红宝石叉瓦擒纵叉,左右交替释放擒纵轮齿,实现等时性计时。',.85);
return g;
}
/* ── balance wheel ── */
function buildBalance(){
const g=new THREE.Group();
g.add(new THREE.Mesh(new THREE.TorusGeometry(.34,.014,8,48),M.gold));
for(let i=0;i<3;i++){const a=i/3*Math.PI*2,arm=new THREE.Mesh(new THREE.BoxGeometry(.018,.32,.022),M.gold);
arm.position.set(Math.cos(a)*.165,Math.sin(a)*.165,0);arm.rotation.z=a+Math.PI/2;g.add(arm);}
for(let i=0;i<8;i++){const a=i/8*Math.PI*2,sc=new THREE.Mesh(new THREE.CylinderGeometry(.011,.011,.022,8).rotateX(Math.PI/2),M.blue);
sc.position.set(Math.cos(a)*.34,Math.sin(a)*.34,0);g.add(sc);}
g.add(new THREE.Mesh(new THREE.CylinderGeometry(.035,.035,.025,16).rotateX(Math.PI/2),M.polish));
const ip=new THREE.Mesh(new THREE.CylinderGeometry(.007,.007,.035,8).rotateX(Math.PI/2),M.ruby);ip.position.y=.045;g.add(ip);
const hs=mkHairspring(.05,.27,9,.003,M.blue);hs.position.z=.018;g.add(hs);
g.position.set(-.82,-.08,.12);
anim.balance=g;
regPart(g,'摆轮游丝','游丝-摆轮振荡系统,频率28,800次/小时(4Hz),蓝钢游丝配KIF避震器,机械表的"心脏"。',1.1);
return g;
}
/* ── bridges ── */
function buildBridges(){
const g=new THREE.Group();
/* balance cock */
const cs=new THREE.Shape();cs.moveTo(-.28,-.12);cs.quadraticCurveTo(-.28,.28,0,.33);cs.quadraticCurveTo(.28,.28,.28,-.12);
cs.lineTo(.18,-.12);cs.lineTo(.14,-.08);cs.lineTo(-.14,-.08);cs.lineTo(-.18,-.12);cs.closePath();
cs.holes.push(circPath(0,0,.05,24));
const ck=new THREE.Mesh(new THREE.ExtrudeGeometry(cs,{depth:.045,bevelEnabled:true,bevelThickness:.004,bevelSize:.004,bevelSegments:1}).translate(0,0,-.0225),M.bridge);
ck.position.set(-.82,-.08,.165);ck.castShadow=true;g.add(ck);
/* main bridge */
const bs=new THREE.Shape();bs.moveTo(-.85,.1);bs.lineTo(-.28,.58);bs.lineTo(.38,.18);bs.lineTo(.48,-.28);bs.lineTo(.08,-.68);bs.lineTo(-.48,-.48);bs.closePath();
[[-.4,.36,.055],[-.62,.48,.035],[.28,-.52,.035],[-.1,.1,.13],[.14,-.18,.11]].forEach(([x,y,r])=>bs.holes.push(circPath(x,y,r,24)));
const bM=new THREE.Mesh(new THREE.ExtrudeGeometry(bs,{depth:.055,bevelEnabled:true,bevelThickness:.004,bevelSize:.004,bevelSegments:1}).translate(0,0,-.0275),M.bridge);
bM.position.z=.115;bM.castShadow=true;g.add(bM);
[[-.24,.48],[.33,.08],[-.38,-.38],[-.68,.04],[-.88,-.14]].forEach(([x,y])=>{const sc=mkScrew(.038);sc.position.set(x,y,.145);g.add(sc);});
regPart(g,'夹板与桥板','桥板装饰日内瓦纹,蓝钢螺丝固定,手工倒角抛光边缘。',.5);
return g;
}
/* ── rubies ── */
function buildRubies(){
const g=new THREE.Group();
[[0,0],[-.62,.48],[.28,-.52],[-.42,-.38],[-.82,-.08],[-.4,.35]].forEach(([x,y])=>{
[[-.05,M.ruby],[.08,M.ruby]].forEach(([dz])=>{
const m=new THREE.Mesh(new THREE.CylinderGeometry(.032,.032,.018,16).rotateX(Math.PI/2),M.ruby);
m.position.set(x,y,dz);g.add(m);
});
});
regPart(g,'红宝石轴承','21颗人造红宝石轴承,有效降低各轮轴摩擦系数,延长机芯寿命。',.3);
return g;
}
/* ── dial markers ── */
function buildMarkers(){
const g=new THREE.Group(),R=1.55;
for(let i=0;i<12;i++){
const a=i/12*Math.PI*2-Math.PI/2,maj=i%3===0,len=maj?.17:.09,w=maj?.038:.023;
const m=new THREE.Mesh(new THREE.BoxGeometry(w,len,.012),M.lume);
m.position.set(Math.cos(a)*(R-len/2),Math.sin(a)*(R-len/2),.34);m.rotation.z=a+Math.PI/2;g.add(m);
}
for(let i=0;i<60;i++){
if(i%5===0)continue;
const a=i/60*Math.PI*2-Math.PI/2;
g.add(new THREE.Mesh(new THREE.CylinderGeometry(.008,.008,.006,8).rotateX(Math.PI/2),M.lume).translateOnAxis(new THREE.Vector3(Math.cos(a),Math.sin(a),0),1.6).translateZ(.34));
}
regPart(g,'时标刻度','Super-LumiNova荧光时标与分钟轨道刻度,暗光环境下清晰可读。',.7);
return g;
}
/* ── hands ── */
function buildHands(){
const g=new THREE.Group();
function hand(pts,z,mat,sc){
const s=new THREE.Shape();s.moveTo(pts[0][0],pts[0][1]);pts.slice(1).forEach(p=>s.lineTo(p[0],p[1]));s.closePath();
const ge=new THREE.ExtrudeGeometry(s,{depth:.02*sc,bevelEnabled:false});ge.translate(0,0,-.01*sc);
const m=new THREE.Mesh(ge,mat);m.position.z=z;return m;
}
const hh=hand([[-.028,-.045],[-.018,.52],[0,.62],[.018,.52],[.028,-.045]],.38,M.polish,1.2);g.add(hh);anim.hour=hh;
const mh=hand([[-.022,-.045],[-.012,.88],[0,1.02],[.012,.88],[.022,-.045]],.4,M.polish,1);g.add(mh);anim.minute=mh;
const sh=hand([[-.007,-.11],[-.004,1.08],[0,1.18],[.004,1.08],[.007,-.11]],.36,M.gold,.8);g.add(sh);anim.second=sh;
g.add(new THREE.Mesh(new THREE.CylinderGeometry(.045,.045,.028,16).rotateX(Math.PI/2),M.polish).translateZ(.42));
regPart(g,'指针','时针与分针精密抛光,玫瑰金秒针,荧光填充确保夜间可读。',1.0);
return g;
}
/* ═══════════════════════ ASSEMBLE ═══════════════════════ */
[buildBack,buildPlate,buildBarrel,buildCenter,buildThird,buildFourth,buildEscape,buildPallet,buildRubies,buildBridges,buildBalance,buildMarkers,buildHands,buildCrystal,buildCase].forEach(fn=>watch.add(fn()));
watch.rotation.x=-.25;
scene.add(watch);
/* floor */
const fl=new THREE.Mesh(new THREE.PlaneGeometry(24,24),new THREE.MeshPhysicalMaterial({color:0x080808,metalness:.4,roughness:.65,envMapIntensity:.2}));
fl.rotation.x=-Math.PI/2;fl.position.y=-3.2;fl.receiveShadow=true;scene.add(fl);
/* ═══════════════════════ ANIMATION ═══════════════════════ */
let eProg=0;
function tick(t){
const s=t*.45;
if(anim.barrel)anim.barrel.rotation.z=s*.05;
if(anim.center)anim.center.rotation.z=-s*.15;
if(anim.third)anim.third.rotation.z=s*.42;
if(anim.fourth)anim.fourth.rotation.z=-s*1;
if(anim.escape)anim.escape.rotation.z=s*3.2;
if(anim.pallet)anim.pallet.rotation.z=Math.sin(s*26)*.14;
if(anim.balance)anim.balance.rotation.z=Math.sin(s*26)*.42;
if(anim.hour)anim.hour.rotation.z=-s*.02;
if(anim.minute)anim.minute.rotation.z=-s*.24;
if(anim.second)anim.second.rotation.z=-s*1;
const tgt=exploded?1:0;eProg+=(tgt-eProg)*.06;
expP.forEach(m=>{const tz=m.userData.oZ+m.userData.eOff*eProg;m.position.z+=(tz-m.position.z)*.08;});
}
/* ═══════════════════════ RAYCAST / INTERACTION ═══════════════════════ */
const rc=new THREE.Raycaster(),mv=new THREE.Vector2();
let hov=null;
addEventListener('mousemove',e=>{mv.x=(e.clientX/innerWidth)*2-1;mv.y=-(e.clientY/innerHeight)*2+1});
addEventListener('click',()=>{
rc.setFromCamera(mv,cam);
const hits=rc.intersectObjects(watch.children,true);
if(hits.length){
let o=hits[0].object;
while(o&&!o.userData.name)o=o.parent;
if(o&&o.userData.name)showIP(o.userData.name,o.userData.desc);
}else hideIP();
});
function showIP(t,d){document.getElementById('iT').textContent=t;document.getElementById('iD').innerHTML=d;document.getElementById('iP').classList.add('show')}
function hideIP(){document.getElementById('iP').classList.remove('show')}
/* ═══════════════════════ 3D LABELS ═══════════════════════ */
function mkLabels(){
const c=document.getElementById('lC');c.innerHTML='';
labParts.forEach(p=>{
if(!p.userData.name)return;
const el=document.createElement('div');el.className='lb';el.textContent=p.userData.name;el.dataset.uid=p.uuid;c.appendChild(el);
});
}
function updLabels(){
if(!showL)return;
document.querySelectorAll('.lb').forEach(el=>{
const p=reg[el.dataset.uid];if(!p)return;
const w=new THREE.Vector3();p.getWorldPosition(w);
const sc=w.project(cam);
if(sc.z>0&&sc.z<1){el.style.left=(sc.x*.5+.5)*innerWidth+'px';el.style.top=(-sc.y*.5+.5)*innerHeight+'px';el.classList.add('v');}
else el.classList.remove('v');
});
}
function rmLabels(){document.getElementById('lC').innerHTML=''}
/* ═══════════════════════ UI GLOBALS ═══════════════════════ */
window.UI={
explode(){exploded=!exploded;document.getElementById('bE').classList.toggle('on',exploded)},
rotate(){ctl.autoRotate=!ctl.autoRotate;document.getElementById('bR').classList.toggle('on',ctl.autoRotate)},
labels(){showL=!showL;document.getElementById('bL').classList.toggle('on',showL);showL?mkLabels():rmLabels()},
};
/* ═══════════════════════ RESIZE ═══════════════════════ */
addEventListener('resize',()=>{cam.aspect=innerWidth/innerHeight;cam.updateProjectionMatrix();R.setSize(innerWidth,innerHeight)});
/* ═══════════════════════ LOOP ═══════════════════════ */
setTimeout(()=>document.getElementById('ld').classList.add('h'),1800);
(function loop(){
requestAnimationFrame(loop);
tick(performance.now()/1000);
ctl.update();
updLabels();
R.render(scene,cam);
})();
</script>
</body>
</html>