Files
nagi-bench/outputs/gemini-3-1-pro/antigravity-high/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

596 lines
33 KiB
HTML

<!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>