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.
596 lines
33 KiB
HTML
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>
|