Files
nagi-bench/outputs/mimo-v2-5-pro-mimocode/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

1091 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Skeleton Watch</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#0a0a0a;font-family:'Helvetica Neue',Arial,sans-serif;color:#e0d5c0}
#canvas{display:block}
#ui{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none}
#title{position:fixed;top:30px;left:50%;transform:translateX(-50%);text-align:center;pointer-events:none;z-index:10}
#title h1{font-size:28px;font-weight:300;letter-spacing:12px;text-transform:uppercase;color:#c8b88a;text-shadow:0 0 20px rgba(200,184,138,0.3)}
#title p{font-size:11px;letter-spacing:6px;color:#8a7e6a;margin-top:6px;text-transform:uppercase}
#info-panel{position:fixed;right:30px;top:50%;transform:translateY(-50%);width:280px;background:rgba(15,13,10,0.92);border:1px solid rgba(200,184,138,0.15);border-radius:12px;padding:24px;pointer-events:auto;backdrop-filter:blur(20px);opacity:0;transition:opacity 0.5s ease;z-index:10}
#info-panel.visible{opacity:1}
#info-panel h2{font-size:14px;letter-spacing:4px;text-transform:uppercase;color:#c8b88a;margin-bottom:16px;font-weight:400}
#info-panel .info-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(200,184,138,0.08)}
#info-panel .info-label{font-size:11px;color:#8a7e6a;letter-spacing:2px;text-transform:uppercase}
#info-panel .info-value{font-size:12px;color:#d4c4a0;text-align:right}
#part-label{position:fixed;pointer-events:none;background:rgba(15,13,10,0.9);border:1px solid rgba(200,184,138,0.3);border-radius:6px;padding:8px 14px;font-size:11px;letter-spacing:2px;text-transform:uppercase;color:#c8b88a;opacity:0;transition:opacity 0.3s;z-index:20;white-space:nowrap}
#part-label::before{content:'';position:absolute;left:-6px;top:50%;transform:translateY(-50%);border:3px solid transparent;border-right-color:rgba(200,184,138,0.3)}
#controls{position:fixed;bottom:30px;left:50%;transform:translateX(-50%);display:flex;gap:12px;pointer-events:auto;z-index:10}
.btn{background:rgba(15,13,10,0.85);border:1px solid rgba(200,184,138,0.2);border-radius:8px;padding:10px 20px;color:#c8b88a;font-size:11px;letter-spacing:3px;text-transform:uppercase;cursor:pointer;transition:all 0.3s;backdrop-filter:blur(10px);font-family:inherit}
.btn:hover{background:rgba(200,184,138,0.15);border-color:rgba(200,184,138,0.4);box-shadow:0 0 15px rgba(200,184,138,0.1)}
.btn.active{background:rgba(200,184,138,0.2);border-color:rgba(200,184,138,0.5)}
#loading{position:fixed;top:0;left:0;width:100%;height:100%;background:#0a0a0a;display:flex;align-items:center;justify-content:center;z-index:100;transition:opacity 1s}
#loading.hidden{opacity:0;pointer-events:none}
#loading .spinner{width:40px;height:40px;border:2px solid rgba(200,184,138,0.1);border-top-color:#c8b88a;border-radius:50%;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
#movement-info{position:fixed;left:30px;bottom:80px;width:240px;background:rgba(15,13,10,0.88);border:1px solid rgba(200,184,138,0.12);border-radius:10px;padding:18px;pointer-events:none;z-index:10;font-size:10px;letter-spacing:1.5px;line-height:2;color:#7a6e5a}
#movement-info .mi-title{font-size:12px;letter-spacing:4px;text-transform:uppercase;color:#c8b88a;margin-bottom:10px}
</style>
</head>
<body>
<div id="loading"><div class="spinner"></div></div>
<div id="title"><h1>Skeleton</h1><p>Haute Horlogerie · Interactive 3D</p></div>
<div id="info-panel">
<h2>Movement Details</h2>
<div class="info-row"><span class="info-label">Caliber</span><span class="info-value">SK-2024A</span></div>
<div class="info-row"><span class="info-label">Type</span><span class="info-value">Manual Wind</span></div>
<div class="info-row"><span class="info-label">Frequency</span><span class="info-value">28,800 vph</span></div>
<div class="info-row"><span class="info-label">Jewels</span><span class="info-value">21 Rubies</span></div>
<div class="info-row"><span class="info-label">Power Reserve</span><span class="info-value">72 Hours</span></div>
<div class="info-row"><span class="info-label">Diameter</span><span class="info-value">39mm</span></div>
<div class="info-row"><span class="info-label">Thickness</span><span class="info-value">8.5mm</span></div>
<div class="info-row"><span class="info-label">Finish</span><span class="info-value">Hand Angled</span></div>
<div class="info-row"><span class="info-label">Case</span><span class="info-value">18K Rose Gold</span></div>
<div class="info-row"><span class="info-label">Crystal</span><span class="info-value">Sapphire</span></div>
<div id="part-detail" style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(200,184,138,0.1);display:none">
<div class="info-row"><span class="info-label">Component</span><span id="pd-name" class="info-value">—</span></div>
<div class="info-row"><span class="info-label">Material</span><span id="pd-material" class="info-value">—</span></div>
<div class="info-row"><span class="info-label">Function</span><span id="pd-function" class="info-value">—</span></div>
</div>
</div>
<div id="movement-info">
<div class="mi-title">Movement Architecture</div>
Main Plate: German Silver<br>
Bridges: Hand-finished, Beveled<br>
Gear Train: 4 Wheels<br>
Escapement: Swiss Lever<br>
Balance: Glucydur<br>
Hairspring: Nivarox<br>
Oscillation: 4 Hz
</div>
<div id="part-label"></div>
<div id="controls">
<button class="btn" id="btn-rotate" onclick="toggleAutoRotate()">Auto Rotate</button>
<button class="btn" id="btn-explode" onclick="toggleExplode()">Exploded View</button>
<button class="btn" id="btn-xray" onclick="toggleXray()">X-Ray Mode</button>
<button class="btn" id="btn-lume" onclick="toggleLume()">Night Mode</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
const {Scene,PerspectiveCamera,WebGLRenderer,MeshPhysicalMaterial,MeshStandardMaterial,MeshPhongMaterial,Mesh,CylinderGeometry,TorusGeometry,SphereGeometry,BoxGeometry,PlaneGeometry,Group,Vector3,Color,AmbientLight,DirectionalLight,PointLight,SpotLight,RectAreaLight,Clock,Raycaster,Vector2,CurvePath,CubicBezierCurve3,Shape,ExtrudeGeometry,LatheGeometry,DoubleSide,FrontSide,AdditiveBlending,Float32BufferAttribute,BufferGeometry,Points,PointsMaterial,LineBasicMaterial,Line,SRGBColorSpace,ACESFilmicToneMapping,PCFSoftShadowMap}=THREE;
let scene,camera,renderer,clock,raycaster,mouse;
let watchGroup,movementGroup,handsGroup;
let autoRotate=true,exploded=false,xrayMode=false,lumeMode=false;
let explodeProgress=0,targetExplode=0;
let hoveredPart=null,selectedPart=null;
let partMap=new Map();
let interactiveParts=[];
let secondAngle=0,minuteAngle=0,hourAngle=0;
let balanceAngle=0,balanceSpeed=0;
let escapeWheelAngle=0;
let palletAngle=0;
let hairspringGroup;
let glowMeshes=[];
init();
animate();
function init(){
scene=new Scene();
scene.background=new Color(0x0a0a0a);
scene.fog=null;
camera=new PerspectiveCamera(35,innerWidth/innerHeight,0.1,100);
camera.position.set(0,2,8);
camera.lookAt(0,0,0);
renderer=new WebGLRenderer({antialias:true,alpha:false,powerPreference:'high-performance'});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.toneMapping=ACESFilmicToneMapping;
renderer.toneMappingExposure=1.2;
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=PCFSoftShadowMap;
renderer.outputColorSpace=SRGBColorSpace;
document.body.appendChild(renderer.domElement);
clock=new Clock();
raycaster=new Raycaster();
mouse=new Vector2(-999,-999);
setupLights();
createWatch();
createEnvironment();
document.addEventListener('mousemove',onMouseMove);
document.addEventListener('click',onClick);
window.addEventListener('resize',onResize);
setTimeout(()=>{
document.getElementById('loading').classList.add('hidden');
},800);
}
function setupLights(){
const amb=new AmbientLight(0x404040,0.4);
scene.add(amb);
const key=new DirectionalLight(0xfff5e6,1.8);
key.position.set(5,8,5);
key.castShadow=true;
key.shadow.mapSize.set(2048,2048);
key.shadow.camera.near=0.5;
key.shadow.camera.far=20;
key.shadow.bias=-0.001;
scene.add(key);
const fill=new DirectionalLight(0xc8d8f0,0.6);
fill.position.set(-4,3,-3);
scene.add(fill);
const rim=new DirectionalLight(0xffe0b0,0.8);
rim.position.set(0,2,-6);
scene.add(rim);
const spot=new SpotLight(0xfff0d0,1.5,20,Math.PI/6,0.5,1);
spot.position.set(0,10,0);
spot.castShadow=true;
scene.add(spot);
const p1=new PointLight(0xff9060,0.3,10);
p1.position.set(-3,2,3);
scene.add(p1);
const p2=new PointLight(0x6090ff,0.2,10);
p2.position.set(3,1,-3);
scene.add(p2);
}
function metalMat(color,roughness=0.15,metalness=0.95){
return new MeshPhysicalMaterial({
color,roughness,metalness,
clearcoat:0.3,clearcoatRoughness:0.1,
reflectivity:1,envMapIntensity:1.5
});
}
function goldMat(){return metalMat(0xc8a050,0.12,0.98)}
function steelMat(){return metalMat(0x8899aa,0.18,0.95)}
function darkSteelMat(){return metalMat(0x3a4050,0.25,0.9)}
function rubyMat(){
return new MeshPhysicalMaterial({
color:0xcc2244,roughness:0.05,metalness:0.1,
transmission:0.6,thickness:0.5,ior:1.77,
clearcoat:1,clearcoatRoughness:0.02,
emissive:0x440010,emissiveIntensity:0.2
});
}
function sapphireMat(){
return new MeshPhysicalMaterial({
color:0xe8e8f0,roughness:0.0,metalness:0.0,
transmission:0.95,thickness:0.3,ior:1.77,
clearcoat:1,clearcoatRoughness:0.0
});
}
function glowMat(color){
return new MeshStandardMaterial({
color,emissive:color,emissiveIntensity:0.8,
roughness:0.4,metalness:0.1
});
}
function registerPart(mesh,name,material,func,category){
const info={name,material,func,category};
partMap.set(mesh.uuid,info);
mesh.userData.partInfo=info;
interactiveParts.push(mesh);
return mesh;
}
function createWatch(){
watchGroup=new Group();
scene.add(watchGroup);
createCase();
createDial();
createMovement();
createHands();
createCrown();
}
function createCase(){
const caseGroup=new Group();
caseGroup.userData.isCase=true;
const caseBody=new Mesh(
new CylinderGeometry(2.0,2.0,0.65,64),
goldMat()
);
caseBody.castShadow=true;
caseBody.receiveShadow=true;
registerPart(caseBody,'Case Middle','18K Rose Gold','Houses the movement and provides structural integrity','case');
caseGroup.add(caseBody);
const bezel=new Mesh(
new TorusGeometry(2.0,0.12,16,64),
goldMat()
);
bezel.position.y=0.35;
registerPart(bezel,'Bezel','18K Rose Gold, Polished','Secures the crystal and frames the dial','case');
caseGroup.add(bezel);
const bezelInner=new Mesh(
new TorusGeometry(1.9,0.06,12,64),
metalMat(0xb08840,0.08,0.98)
);
bezelInner.position.y=0.38;
caseGroup.add(bezelInner);
const caseback=new Mesh(
new CylinderGeometry(1.95,1.95,0.08,64),
metalMat(0xb09050,0.2,0.95)
);
caseback.position.y=-0.35;
caseback.receiveShadow=true;
registerPart(caseback,'Case Back','Sapphire Crystal, Rose Gold Frame','Exhibition case back revealing the movement','case');
caseGroup.add(caseback);
const lugGeom=new BoxGeometry(0.35,0.5,0.8);
const lugPositions=[
{x:1.7,z:0.9,r:0.2},{x:-1.7,z:0.9,r:-0.2},
{x:1.7,z:-0.9,r:-0.2},{x:-1.7,z:-0.9,r:0.2}
];
lugPositions.forEach(p=>{
const lug=new Mesh(lugGeom,goldMat());
lug.position.set(p.x,0,p.z);
lug.rotation.y=p.r;
lug.castShadow=true;
registerPart(lug,'Lug','18K Rose Gold','Connects the case to the strap','case');
caseGroup.add(lug);
});
const crystal=new Mesh(
new CylinderGeometry(1.85,1.85,0.06,64),
sapphireMat()
);
crystal.position.y=0.42;
registerPart(caseGroup,'Sapphire Crystal','Sapphire Glass, AR Coated','Anti-reflective coated crystal for clear view of dial','case');
caseGroup.add(crystal);
watchGroup.add(caseGroup);
}
function createDial(){
const dialGroup=new Group();
dialGroup.position.y=0.2;
const hourMarkers=[];
for(let i=0;i<12;i++){
const angle=(i/12)*Math.PI*2;
const isMain=i%3===0;
const len=isMain?0.3:0.18;
const w=isMain?0.06:0.035;
const marker=new Mesh(
new BoxGeometry(w,0.02,len),
glowMat(0xc8b88a)
);
marker.position.set(Math.sin(angle)*1.55,0,Math.cos(angle)*1.55);
marker.rotation.y=-angle;
marker.castShadow=true;
registerPart(marker,`Hour Marker ${i===0?12:i}`,'Luminous Gold','Indicates the hour position','dial');
dialGroup.add(marker);
glowMeshes.push(marker);
if(!isMain){
for(let j=1;j<5;j++){
const subAngle=angle+(j/5)*((Math.PI*2)/12);
const tick=new Mesh(
new BoxGeometry(0.015,0.01,0.06),
metalMat(0xa09070,0.3,0.8)
);
tick.position.set(Math.sin(subAngle)*1.6,0,Math.cos(subAngle)*1.6);
tick.rotation.y=-subAngle;
dialGroup.add(tick);
}
}
}
const minuteTrack=new Mesh(
new TorusGeometry(1.65,0.01,8,120),
metalMat(0x998866,0.3,0.7)
);
minuteTrack.position.y=0.01;
minuteTrack.rotation.x=Math.PI/2;
dialGroup.add(minuteTrack);
const innerTrack=new Mesh(
new TorusGeometry(1.2,0.008,8,100),
metalMat(0x887755,0.4,0.6)
);
innerTrack.position.y=0.01;
innerTrack.rotation.x=Math.PI/2;
dialGroup.add(innerTrack);
watchGroup.add(dialGroup);
}
function createMovement(){
movementGroup=new Group();
movementGroup.position.y=-0.05;
createMainplate();
createBarrelBridge();
createGearTrain();
createEscapement();
createBalanceWheel();
createBridges();
createRubyBearings();
watchGroup.add(movementGroup);
}
function createMainplate(){
const plate=new Mesh(
new CylinderGeometry(1.75,1.75,0.08,64),
darkSteelMat()
);
plate.receiveShadow=true;
registerPart(plate,'Main Plate','German Silver (Maillechort)','Foundation plate supporting all movement components','movement');
movementGroup.add(plate);
for(let i=0;i<8;i++){
const a=(i/8)*Math.PI*2;
const cutout=new Mesh(
new CylinderGeometry(0.15+Math.random()*0.1,0.15+Math.random()*0.1,0.12,6),
darkSteelMat()
);
cutout.position.set(Math.sin(a)*1.2,0,Math.cos(a)*1.2);
movementGroup.add(cutout);
}
}
function createBarrelBridge(){
const barrelGroup=new Group();
const barrelDrum=new Mesh(
new CylinderGeometry(0.5,0.5,0.2,32),
steelMat()
);
barrelDrum.position.set(-0.9,0.15,0);
registerPart(barrelDrum,'Mainspring Barrel','Polished Steel','Stores potential energy from winding via coiled mainspring','movement');
barrelGroup.add(barrelDrum);
const barrelArbor=new Mesh(
new CylinderGeometry(0.06,0.06,0.35,16),
metalMat(0x667788,0.1,0.95)
);
barrelArbor.position.set(-0.9,0.2,0);
barrelGroup.add(barrelArbor);
const barrelTop=new Mesh(
new CylinderGeometry(0.48,0.48,0.03,32),
metalMat(0x778899,0.15,0.9)
);
barrelTop.position.set(-0.9,0.28,0);
registerPart(barrelTop,'Barrel Bridge','German Silver, Beveled','Secures the mainspring barrel to the mainplate','movement');
barrelGroup.add(barrelTop);
for(let i=0;i<30;i++){
const r=0.15+Math.random()*0.3;
const a=Math.random()*Math.PI*2;
const coil=new Mesh(
new TorusGeometry(r,0.008,4,20),
metalMat(0x556677,0.3,0.85)
);
coil.position.set(-0.9,0.12+i*0.003,0);
coil.rotation.x=Math.PI/2;
barrelGroup.add(coil);
}
movementGroup.add(barrelGroup);
}
function createGearTrain(){
const gears=[
{name:'Center Wheel',r:0.35,x:0,z:0,teeth:36,h:0.12,speed:1,material:'Blued Steel'},
{name:'Third Wheel',r:0.25,x:0.55,z:0.4,teeth:28,h:0.1,speed:2.5,material:'Blued Steel'},
{name:'Fourth Wheel',r:0.18,x:0.75,z:-0.25,teeth:24,h:0.08,speed:8,material:'Blued Steel'},
{name:'Escape Wheel',r:0.2,x:0.4,z:-0.65,teeth:20,h:0.06,speed:16,material:'Hardened Steel'}
];
gears.forEach(g=>{
const group=new Group();
group.position.set(g.x,0.18,g.z);
const wheel=new Mesh(
new CylinderGeometry(g.r,g.r,g.h,32),
steelMat()
);
wheel.castShadow=true;
registerPart(wheel,g.name,g.material,
g.name==='Center Wheel'?'Driven by barrel, carries minute hand':
g.name==='Third Wheel'?'Intermediate wheel transferring energy':
g.name==='Fourth Wheel'?'Drives seconds hand, connects to escapement':
'Part of the Swiss lever escapement mechanism','movement');
group.add(wheel);
for(let i=0;i<g.teeth;i++){
const a=(i/g.teeth)*Math.PI*2;
const tooth=new Mesh(
new BoxGeometry(0.015,g.h+0.01,0.04),
steelMat()
);
tooth.position.set(Math.sin(a)*(g.r+0.02),0,Math.cos(a)*(g.r+0.02));
tooth.rotation.y=-a;
group.add(tooth);
}
const hub=new Mesh(
new CylinderGeometry(0.04,0.04,g.h+0.05,12),
metalMat(0x99aabb,0.1,0.95)
);
group.add(hub);
const pinion=new Mesh(
new CylinderGeometry(0.06,0.06,0.08,8),
metalMat(0x778899,0.15,0.9)
);
pinion.position.y=0.05;
group.add(pinion);
group.userData.gearSpeed=g.speed;
group.userData.gearName=g.name;
movementGroup.add(group);
});
}
function createEscapement(){
const escGroup=new Group();
escGroup.position.set(0.4,0.25,-0.65);
const palletFork=new Group();
const forkBody=new Mesh(
new BoxGeometry(0.04,0.06,0.4),
steelMat()
);
registerPart(forkBody,'Pallet Fork','Hardened Steel','Transfers impulse from escape wheel to balance wheel via entry and exit pallets','movement');
palletFork.add(forkBody);
const entryPallet=new Mesh(
new BoxGeometry(0.08,0.05,0.04),
rubyMat()
);
entryPallet.position.set(0,0,0.18);
palletFork.add(entryPallet);
const exitPallet=new Mesh(
new BoxGeometry(0.08,0.05,0.04),
rubyMat()
);
exitPallet.position.set(0,0,-0.18);
palletFork.add(exitPallet);
const guardPin=new Mesh(
new CylinderGeometry(0.01,0.01,0.08,8),
steelMat()
);
guardPin.position.set(0,0.04,0);
palletFork.add(guardPin);
const palletPivot=new Mesh(
new CylinderGeometry(0.015,0.015,0.15,8),
metalMat(0x8899aa,0.1,0.95)
);
palletFork.add(palletPivot);
palletFork.userData.isPallet=true;
escGroup.add(palletFork);
movementGroup.add(escGroup);
}
function createBalanceWheel(){
const balanceGroup=new Group();
balanceGroup.position.set(0.4,0.35,-0.65);
const rim=new Mesh(
new TorusGeometry(0.35,0.025,12,48),
metalMat(0xccbb88,0.1,0.97)
);
rim.rotation.x=Math.PI/2;
registerPart(rim,'Balance Rim','Glucydur Alloy','Oscillating rim governing timekeeping precision','movement');
balanceGroup.add(rim);
const spokeCount=3;
for(let i=0;i<spokeCount;i++){
const a=(i/spokeCount)*Math.PI*2;
const spoke=new Mesh(
new BoxGeometry(0.02,0.02,0.34),
metalMat(0xbbaa77,0.12,0.96)
);
spoke.position.set(Math.sin(a)*0.17,0,Math.cos(a)*0.17);
spoke.rotation.y=-a;
balanceGroup.add(spoke);
}
const hub=new Mesh(
new CylinderGeometry(0.05,0.05,0.06,16),
metalMat(0xccbb88,0.1,0.97)
);
balanceGroup.add(hub);
for(let i=0;i<4;i++){
const a=(i/4)*Math.PI*2+Math.PI/4;
const screw=new Mesh(
new CylinderGeometry(0.018,0.018,0.025,8),
metalMat(0xddcc99,0.05,0.98)
);
screw.position.set(Math.sin(a)*0.3,0,Math.cos(a)*0.3);
balanceGroup.add(screw);
}
hairspringGroup=new Group();
const spiralTurns=8;
const spiralPoints=[];
for(let i=0;i<=spiralTurns*60;i++){
const t=i/(spiralTurns*60);
const angle=t*spiralTurns*Math.PI*2;
const r=0.04+t*0.25;
spiralPoints.push(new Vector3(Math.cos(angle)*r,0,Math.sin(angle)*r));
}
for(let i=0;i<spiralPoints.length-1;i++){
const geo=new CylinderGeometry(0.003,0.003,spiralPoints[i].distanceTo(spiralPoints[i+1]),4);
const seg=new Mesh(geo,metalMat(0x88aacc,0.2,0.85));
const mid=new Vector3().addVectors(spiralPoints[i],spiralPoints[i+1]).multiplyScalar(0.5);
seg.position.copy(mid);
seg.lookAt(spiralPoints[i+1]);
seg.rotateX(Math.PI/2);
hairspringGroup.add(seg);
}
registerPart(hairspringGroup.children[0]||hairspringGroup,'Hairspring (Spiral)','Nivarox Alloy','Ultra-thin spring that oscillates to regulate the balance wheel frequency','movement');
balanceGroup.add(hairspringGroup);
const collet=new Mesh(
new CylinderGeometry(0.035,0.035,0.03,12),
metalMat(0xccbb88,0.1,0.97)
);
collet.position.y=0.02;
balanceGroup.add(collet);
balanceGroup.userData.isBalance=true;
movementGroup.add(balanceGroup);
}
function createBridges(){
const bridgeData=[
{name:'Balance Cock',x:0.4,z:-0.65,w:0.6,d:0.5,h:0.06,rot:0.3},
{name:'Gear Train Bridge',x:0.55,z:0.1,w:1.0,d:0.5,h:0.05,rot:0.1},
{name:'Barrel Bridge',x:-0.9,z:0,w:0.7,d:0.6,h:0.05,rot:-0.2}
];
bridgeData.forEach(b=>{
const bridge=new Mesh(
new BoxGeometry(b.w,b.h,b.d),
metalMat(0x8a9aaa,0.2,0.88)
);
bridge.position.set(b.x,0.28,b.z);
bridge.rotation.y=b.rot;
bridge.castShadow=true;
registerPart(bridge,b.name,'German Silver, Hand-Finished','Secures movement components and provides structural support','movement');
movementGroup.add(bridge);
const bevel=0.01;
const edge=new Mesh(
new BoxGeometry(b.w+bevel*2,b.h*0.3,b.d+bevel*2),
metalMat(0x9aaaba,0.15,0.9)
);
edge.position.set(b.x,0.28+b.h*0.35,b.z);
edge.rotation.y=b.rot;
movementGroup.add(edge);
for(let i=0;i<3;i++){
const sx=b.x+(Math.random()-0.5)*b.w*0.6;
const sz=b.z+(Math.random()-0.5)*b.d*0.6;
const screw=new Mesh(
new CylinderGeometry(0.025,0.025,0.04,12),
metalMat(0x6677cc,0.05,0.95)
);
screw.position.set(sx,0.32,sz);
movementGroup.add(screw);
const slot=new Mesh(
new BoxGeometry(0.04,0.005,0.008),
metalMat(0x4455aa,0.1,0.9)
);
slot.position.set(sx,0.34,sz);
movementGroup.add(slot);
}
});
}
function createRubyBearings(){
const rubyPositions=[
{x:0,y:0.1,z:0,label:'Center Wheel Jewel'},
{x:0.55,y:0.1,z:0.4,label:'Third Wheel Jewel'},
{x:0.75,y:0.1,z:-0.25,label:'Fourth Wheel Jewel'},
{x:0.4,y:0.1,z:-0.65,label:'Escape Wheel Jewel'},
{x:0.4,y:0.28,z:-0.65,label:'Balance Jewel (Upper)'},
{x:0.4,y:0.08,z:-0.65,label:'Balance Jewel (Lower)'},
{x:-0.9,y:0.1,z:0,label:'Barrel Jewel'}
];
rubyPositions.forEach(r=>{
const setting=new Mesh(
new CylinderGeometry(0.055,0.055,0.025,16),
goldMat()
);
setting.position.set(r.x,r.y,r.z);
movementGroup.add(setting);
const jewel=new Mesh(
new CylinderGeometry(0.035,0.035,0.02,16),
rubyMat()
);
jewel.position.set(r.x,r.y+0.01,r.z);
registerPart(jewel,r.label,'Synthetic Ruby','Reduces friction at pivot point, provides shock absorption','movement');
movementGroup.add(jewel);
const glow=new Mesh(
new CylinderGeometry(0.06,0.06,0.005,16),
new MeshStandardMaterial({color:0xff2244,emissive:0xff0033,emissiveIntensity:0.3,transparent:true,opacity:0.4})
);
glow.position.set(r.x,r.y+0.015,r.z);
glowMeshes.push(glow);
movementGroup.add(glow);
});
}
function createHands(){
handsGroup=new Group();
handsGroup.position.y=0.25;
const hourHand=new Group();
const hGeo=new Shape();
hGeo.moveTo(0,-0.04);
hGeo.lineTo(0.7,0);
hGeo.lineTo(0,-0.04);
hGeo.lineTo(0.65,0);
hGeo.lineTo(0,0.04);
hGeo.lineTo(-0.05,0);
const hourShape=new Shape();
hourShape.moveTo(-0.05,0);
hourShape.lineTo(0,-0.045);
hourShape.lineTo(0.68,-0.012);
hourShape.lineTo(0.7,0);
hourShape.lineTo(0.68,0.012);
hourShape.lineTo(0,0.045);
hourShape.lineTo(-0.05,0);
const hMesh=new Mesh(
new ExtrudeGeometry(hourShape,{depth:0.025,bevelEnabled:true,bevelSize:0.005,bevelThickness:0.005,bevelSegments:2}),
glowMat(0xd4c090)
);
hMesh.rotation.x=-Math.PI/2;
hMesh.position.y=0.05;
registerPart(hMesh,'Hour Hand','18K Gold, Luminous','Points to current hour with SuperLuminova coating','hands');
hourHand.add(hMesh);
glowMeshes.push(hMesh);
const minuteShape=new Shape();
minuteShape.moveTo(-0.04,0);
minuteShape.lineTo(0,-0.035);
minuteShape.lineTo(0.95,-0.008);
minuteShape.lineTo(0.97,0);
minuteShape.lineTo(0.95,0.008);
minuteShape.lineTo(0,0.035);
minuteShape.lineTo(-0.04,0);
const minMesh=new Mesh(
new ExtrudeGeometry(minuteShape,{depth:0.02,bevelEnabled:true,bevelSize:0.004,bevelThickness:0.004,bevelSegments:2}),
glowMat(0xd4c090)
);
minMesh.rotation.x=-Math.PI/2;
minMesh.position.y=0.08;
registerPart(minMesh,'Minute Hand','18K Gold, Luminous','Points to current minute with fine finishing','hands');
handsGroup.add(minMesh);
glowMeshes.push(minMesh);
handsGroup.add(hourHand);
const secShape=new Shape();
secShape.moveTo(-0.02,0);
secShape.lineTo(0,-0.015);
secShape.lineTo(1.1,-0.003);
secShape.lineTo(1.15,0);
secShape.lineTo(1.1,0.003);
secShape.lineTo(0,0.015);
secShape.lineTo(-0.02,0);
const secMesh=new Mesh(
new ExtrudeGeometry(secShape,{depth:0.012,bevelEnabled:false}),
glowMat(0xe04040)
);
secMesh.rotation.x=-Math.PI/2;
secMesh.position.y=0.11;
registerPart(secMesh,'Second Hand','Blued Steel, Red Tip','Sweeping seconds hand with counterweight','hands');
handsGroup.add(secMesh);
const centerCap=new Mesh(
new CylinderGeometry(0.06,0.06,0.05,16),
goldMat()
);
centerCap.position.y=0.07;
handsGroup.add(centerCap);
hourHand.userData.isHourHand=true;
handsGroup.add(hourHand);
watchGroup.add(handsGroup);
}
function createCrown(){
const crownGroup=new Group();
crownGroup.position.set(2.2,0,0);
const crown=new Mesh(
new CylinderGeometry(0.12,0.14,0.25,24),
goldMat()
);
crown.rotation.z=Math.PI/2;
registerPart(crown,'Crown','18K Rose Gold','Winds the mainspring and sets the time','case');
crownGroup.add(crown);
for(let i=0;i<20;i++){
const a=(i/20)*Math.PI*2;
const grip=new Mesh(
new BoxGeometry(0.01,0.26,0.015),
metalMat(0xb08840,0.15,0.98)
);
grip.position.set(0,Math.sin(a)*0.13,Math.cos(a)*0.13);
grip.rotation.x=a;
crownGroup.add(grip);
}
const crownCap=new Mesh(
new CylinderGeometry(0.06,0.06,0.04,16),
metalMat(0xc09050,0.1,0.98)
);
crownCap.rotation.z=Math.PI/2;
crownCap.position.x=0.14;
crownGroup.add(crownCap);
watchGroup.add(crownGroup);
}
function createEnvironment(){
const floor=new Mesh(
new PlaneGeometry(30,30),
new MeshStandardMaterial({
color:0x080808,roughness:0.4,metalness:0.6,
envMapIntensity:0.5
})
);
floor.rotation.x=-Math.PI/2;
floor.position.y=-1.2;
floor.receiveShadow=true;
scene.add(floor);
const gridHelper=new Mesh(
new PlaneGeometry(20,20),
new MeshStandardMaterial({
color:0x111111,roughness:0.8,metalness:0.2,
transparent:true,opacity:0.3
})
);
gridHelper.rotation.x=-Math.PI/2;
gridHelper.position.y=-1.19;
scene.add(gridHelper);
const ringGeo=new TorusGeometry(4,0.005,8,128);
for(let i=0;i<3;i++){
const ring=new Mesh(ringGeo,new MeshStandardMaterial({color:0xc8b88a,transparent:true,opacity:0.03+i*0.01,emissive:0xc8b88a,emissiveIntensity:0.1}));
ring.rotation.x=Math.PI/2;
ring.position.y=-1.18;
ring.scale.setScalar(1+i*0.5);
scene.add(ring);
}
}
function onMouseMove(e){
mouse.x=(e.clientX/innerWidth)*2-1;
mouse.y=-(e.clientY/innerHeight)*2+1;
const label=document.getElementById('part-label');
raycaster.setFromCamera(mouse,camera);
const intersects=raycaster.intersectObjects(interactiveParts,true);
if(intersects.length>0){
const hit=intersects[0].object;
let info=hit.userData.partInfo;
if(!info){
for(const child of interactiveParts){
if(child.children&&child.children.includes(hit)){
info=child.userData.partInfo;
break;
}
}
}
if(info){
label.textContent=info.name;
label.style.left=e.clientX+20+'px';
label.style.top=e.clientY-15+'px';
label.style.opacity='1';
document.body.style.cursor='pointer';
if(hoveredPart!==hit){
resetHighlight();
hoveredPart=hit;
highlightPart(hit,true);
}
}else{
clearHover();
}
}else{
clearHover();
}
}
function clearHover(){
const label=document.getElementById('part-label');
label.style.opacity='0';
document.body.style.cursor='default';
if(hoveredPart){
resetHighlight();
hoveredPart=null;
}
}
function highlightPart(mesh,hover){
if(mesh.material){
mesh.userData.origEmissive=mesh.material.emissive?mesh.material.emissive.clone():null;
mesh.userData.origEmissiveIntensity=mesh.material.emissiveIntensity;
if(hover){
mesh.material.emissive=new Color(0xc8b88a);
mesh.material.emissiveIntensity=0.3;
}
}
}
function resetHighlight(){
if(hoveredPart&&hoveredPart.material){
if(hoveredPart.userData.origEmissive){
hoveredPart.material.emissive.copy(hoveredPart.userData.origEmissive);
hoveredPart.material.emissiveIntensity=hoveredPart.userData.origEmissiveIntensity||0;
}
}
}
function onClick(e){
raycaster.setFromCamera(mouse,camera);
const intersects=raycaster.intersectObjects(interactiveParts,true);
const panel=document.getElementById('info-panel');
const partDetail=document.getElementById('part-detail');
if(intersects.length>0){
const hit=intersects[0].object;
let info=hit.userData.partInfo;
if(!info){
for(const child of interactiveParts){
if(child.children&&child.children.includes(hit)){
info=child.userData.partInfo;break;
}
}
}
if(info){
selectedPart=hit;
document.getElementById('pd-name').textContent=info.name;
document.getElementById('pd-material').textContent=info.material;
document.getElementById('pd-function').textContent=info.func;
partDetail.style.display='block';
panel.classList.add('visible');
}
}
}
function toggleAutoRotate(){
autoRotate=!autoRotate;
document.getElementById('btn-rotate').classList.toggle('active',autoRotate);
}
document.getElementById('btn-rotate').classList.add('active');
function toggleExplode(){
exploded=!exploded;
targetExplode=exploded?1:0;
document.getElementById('btn-explode').classList.toggle('active',exploded);
}
function toggleXray(){
xrayMode=!xrayMode;
document.getElementById('btn-xray').classList.toggle('active',xrayMode);
watchGroup.traverse(child=>{
if(child.isMesh&&child.material){
if(child.material.transmission!==undefined){
if(xrayMode){
child.userData.origOpacity=child.material.opacity;
child.userData.origTransmission=child.material.transmission;
child.material.transparent=true;
child.material.opacity=0.15;
child.material.needsUpdate=true;
}else if(child.userData.origOpacity!==undefined){
child.material.opacity=child.userData.origOpacity;
child.material.transmission=child.userData.origTransmission;
child.material.needsUpdate=true;
}
}
}
});
}
function toggleLume(){
lumeMode=!lumeMode;
document.getElementById('btn-lume').classList.toggle('active',lumeMode);
glowMeshes.forEach(m=>{
if(m.material&&m.material.emissive){
m.material.emissiveIntensity=lumeMode?2.5:0.8;
}
});
if(lumeMode){
scene.background=new Color(0x020204);
}else{
scene.background=new Color(0x0a0a0a);
}
}
function animate(){
requestAnimationFrame(animate);
const dt=clock.getDelta();
const t=clock.getElapsedTime();
if(autoRotate){
watchGroup.rotation.y+=dt*0.15;
}
watchGroup.rotation.x=Math.sin(t*0.3)*0.05+0.3;
const breathe=Math.sin(t*1.5)*0.002;
watchGroup.position.y=breathe;
secondAngle+=dt*Math.PI*2/60;
minuteAngle+=dt*Math.PI*2/3600;
hourAngle+=dt*Math.PI*2/43200;
handsGroup.children.forEach(child=>{
if(child.userData.isHourHand){
child.rotation.y=hourAngle;
}
});
const minHand=handsGroup.children.find(c=>!c.userData.isHourHand&&c.geometry);
const secHand=handsGroup.children[handsGroup.children.length-2];
if(handsGroup.children.length>=2){
handsGroup.children[0].rotation.y=hourAngle;
}
balanceSpeed=Math.sin(t*4)*3;
balanceAngle=Math.sin(t*8)*0.8;
movementGroup.children.forEach(child=>{
if(child.userData.isBalance){
child.rotation.y=balanceAngle;
if(hairspringGroup){
hairspringGroup.scale.setScalar(1+Math.sin(t*8)*0.03);
}
}
if(child.userData.isPallet){
child.rotation.y=Math.sin(t*8)*0.3;
}
if(child.userData.gearSpeed){
child.rotation.y+=dt*child.userData.gearSpeed*0.3;
}
});
explodeProgress+=(targetExplode-explodeProgress)*dt*3;
if(explodeProgress>0.01){
const layers=[
{parts:'case',offset:new Vector3(0,explodeProgress*1.5,0)},
];
watchGroup.children.forEach(child=>{
if(child.userData.isCase){
child.position.y=explodeProgress*1.2;
child.children.forEach(c=>{
if(c.position.y>0.3)c.position.y=c.userData.origY!==undefined?c.userData.origY+explodeProgress*0.8:0.42+explodeProgress*0.8;
});
}
});
movementGroup.children.forEach((child,i)=>{
if(!child.userData.origPos){
child.userData.origPos=child.position.clone();
}
const dir=child.userData.origPos.clone().normalize();
const verticalOffset=new Vector3(0,explodeProgress*0.3*(i%2===0?1:-1),0);
child.position.copy(child.userData.origPos).add(dir.multiplyScalar(explodeProgress*0.8)).add(verticalOffset);
});
handsGroup.position.y=0.25+explodeProgress*2.0;
}else{
movementGroup.children.forEach(child=>{
if(child.userData.origPos){
child.position.copy(child.userData.origPos);
}
});
handsGroup.position.y=0.25;
}
renderer.render(scene,camera);
}
function onResize(){
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
}
let isDragging=false;
let prevMouse={x:0,y:0};
let rotY=0,rotX=0.3;
renderer.domElement.addEventListener('pointerdown',e=>{
isDragging=true;
prevMouse={x:e.clientX,y:e.clientY};
renderer.domElement.setPointerCapture(e.pointerId);
});
renderer.domElement.addEventListener('pointermove',e=>{
if(!isDragging)return;
const dx=e.clientX-prevMouse.x;
const dy=e.clientY-prevMouse.y;
rotY+=dx*0.005;
rotX+=dy*0.005;
rotX=Math.max(-1,Math.min(1,rotX));
watchGroup.rotation.y=rotY;
watchGroup.rotation.x=rotX;
prevMouse={x:e.clientX,y:e.clientY};
autoRotate=false;
document.getElementById('btn-rotate').classList.remove('active');
});
renderer.domElement.addEventListener('pointerup',()=>{
isDragging=false;
});
renderer.domElement.addEventListener('wheel',e=>{
camera.position.z+=e.deltaY*0.005;
camera.position.z=Math.max(4,Math.min(15,camera.position.z));
},{passive:true});
document.getElementById('info-panel').classList.add('visible');
</script>
</body>
</html>