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:
1 parent
c9cf217db3
commit
75b18a76e4
16 files changed
+8031
-16
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user