contrib: MiMo V2.5 (High) all cases

Adds MiMo V2.5 (High) MiMo Code artifacts for mythos-craft, pelican-cycling, and skeleton-watch. Maintainer-adjusted skeleton-watch visible calibre label to avoid blind Arena identity leakage.
This commit is contained in:
BaiLin-William authored and GitHub committed 2026-07-02 17:15:27 +01:00
1 parent acb27270f0
commit 1a6b884f75
4 files changed
+1762

No files matched your search

@@ -0,0 +1,774 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skeleton Watch - 3D Mechanical Timepiece</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#0a0a0f;font-family:'Helvetica Neue',Arial,sans-serif;color:#fff}
canvas{display:block}
#blocker{position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:100;cursor:pointer}
#blocker .inner{text-align:center;max-width:500px;padding:40px}
#blocker h1{font-size:42px;font-weight:200;letter-spacing:8px;color:#c9a96e;margin-bottom:8px}
#blocker h2{font-size:14px;color:#888;font-weight:300;letter-spacing:3px;margin-bottom:30px}
#blocker .controls{text-align:left;background:rgba(255,255,255,0.03);border:1px solid rgba(201,169,110,0.2);border-radius:8px;padding:20px;margin-bottom:24px;line-height:2.2;font-size:13px;color:#999}
#blocker .controls span{color:#c9a96e;font-weight:500}
#blocker .start-btn{display:inline-block;padding:12px 40px;background:transparent;color:#c9a96e;font-size:14px;font-weight:300;letter-spacing:4px;border:1px solid #c9a96e;border-radius:4px;cursor:pointer;transition:all 0.3s}
#blocker .start-btn:hover{background:#c9a96e;color:#0a0a0f}
#ui{position:fixed;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:10}
#info-panel{position:fixed;top:20px;right:20px;width:260px;background:rgba(10,10,15,0.85);border:1px solid rgba(201,169,110,0.15);border-radius:6px;padding:16px;pointer-events:auto;font-size:12px;line-height:1.8;color:#aaa;backdrop-filter:blur(10px);display:none}
#info-panel h3{color:#c9a96e;font-size:13px;font-weight:400;letter-spacing:2px;margin-bottom:8px;border-bottom:1px solid rgba(201,169,110,0.2);padding-bottom:6px}
#info-panel .row{display:flex;justify-content:space-between}
#info-panel .label{color:#666}
#info-panel .value{color:#ccc}
#controls-bar{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:flex;gap:12px;pointer-events:auto}
#controls-bar button{padding:8px 20px;background:rgba(10,10,15,0.8);border:1px solid rgba(201,169,110,0.3);border-radius:4px;color:#c9a96e;font-size:12px;letter-spacing:2px;cursor:pointer;transition:all 0.3s;backdrop-filter:blur(5px)}
#controls-bar button:hover{background:rgba(201,169,110,0.15);border-color:#c9a96e}
#controls-bar button.active{background:rgba(201,169,110,0.2);border-color:#c9a96e}
#label-tooltip{position:fixed;pointer-events:none;background:rgba(10,10,15,0.9);border:1px solid rgba(201,169,110,0.3);border-radius:4px;padding:6px 12px;font-size:11px;color:#c9a96e;letter-spacing:1px;display:none;white-space:nowrap;z-index:20;backdrop-filter:blur(5px)}
#clock-display{position:fixed;top:20px;left:20px;font-size:11px;color:rgba(201,169,110,0.5);letter-spacing:3px;font-weight:300;display:none}
</style>
</head>
<body>
<div id="blocker">
<div class="inner">
<h1>SKELETON</h1>
<h2>3D MECHANICAL TIMEPIECE</h2>
<div class="controls">
<span>拖拽</span> - 旋转手表 Rotate<br>
<span>滚轮</span> - 缩放 Zoom<br>
<span>E</span> - 爆炸视图 Exploded View<br>
<span>I</span> - 机芯信息 Movement Info<br>
<span>空格</span> - 暂停/恢复动画 Pause/Resume
</div>
<button class="start-btn" id="startBtn">ENTER</button>
</div>
</div>
<div id="ui"></div>
<div id="info-panel">
<h3>MOVEMENT DATA</h3>
<div class="row"><span class="label">Caliber</span><span class="value">SK-01</span></div>
<div class="row"><span class="label">Frequency</span><span class="value">28,800 vph</span></div>
<div class="row"><span class="label">Jewels</span><span class="value">35 Rubies</span></div>
<div class="row"><span class="label">Power Reserve</span><span class="value">72 Hours</span></div>
<div class="row"><span class="label">Components</span><span class="value">287 Parts</span></div>
<div class="row"><span class="label">Diameter</span><span class="value">30.4mm</span></div>
<div class="row"><span class="label">Height</span><span class="value">5.35mm</span></div>
<div class="row"><span class="label">Finish</span><span class="value">Côtes de Genève</span></div>
<div class="row"><span class="label">Balance</span><span class="value">Free-sprung</span></div>
</div>
<div id="controls-bar">
<button id="btn-explode">EXPLODE</button>
<button id="btn-info">INFO</button>
<button id="btn-pause">PAUSE</button>
</div>
<div id="label-tooltip"></div>
<div id="clock-display">00:00:00</div>
<script>/**
* @license
* Copyright 2010-2021 Three.js Authors
* SPDX-License-Identifier: MIT
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="128",n=100,i=300,r=301,s=302,a=303,o=304,l=306,c=307,h=1e3,u=1001,d=1002,p=1003,m=1004,f=1005,g=1006,v=1007,y=1008,x=1009,_=1012,w=1014,b=1015,M=1016,S=1020,T=1022,E=1023,A=1026,L=1027,R=33776,C=33777,P=33778,D=33779,I=35840,N=35841,B=35842,z=35843,F=37492,O=37496,H=2300,G=2301,U=2302,k=2400,V=2401,W=2402,j=2500,q=2501,X=3e3,Y=3001,Z=3007,J=3002,Q=3004,K=3005,$=3006,tt=7680,et=35044,nt=35048,it="300 es";class rt{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e<i;e++)n[e].call(this,t);t.target=null}}}const st=[];for(let t=0;t<256;t++)st[t]=(t<16?"0":"")+t.toString(16);let at=1234567;const ot=Math.PI/180,lt=180/Math.PI;function ct(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,i=4294967295*Math.random()|0;return(st[255&t]+st[t>>8&255]+st[t>>16&255]+st[t>>24&255]+"-"+st[255&e]+st[e>>8&255]+"-"+st[e>>16&15|64]+st[e>>24&255]+"-"+st[63&n|128]+st[n>>8&255]+"-"+st[n>>16&255]+st[n>>24&255]+st[255&i]+st[i>>8&255]+st[i>>16&255]+st[i>>24&255]).toUpperCase()}function ht(t,e,n){return Math.max(e,Math.min(n,t))}function ut(t,e){return(t%e+e)%e}function dt(t,e,n){return(1-n)*t+n*e}function pt(t){return 0==(t&t-1)&&0!==t}function mt(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))}function ft(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}var gt=Object.freeze({__proto__:null,DEG2RAD:ot,RAD2DEG:lt,generateUUID:ct,clamp:ht,euclideanModulo:ut,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:dt,damp:function(t,e,n,i){return dt(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(ut(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){return void 0!==t&&(at=t%2147483647),at=16807*at%2147483647,(at-1)/2147483646},degToRad:function(t){return t*ot},radToDeg:function(t){return t*lt},isPowerOfTwo:pt,ceilPowerOfTwo:mt,floorPowerOfTwo:ft,setQuaternionFromProperEuler:function(t,e,n,i,r){const s=Math.cos,a=Math.sin,o=s(n/2),l=a(n/2),c=s((e+i)/2),h=a((e+i)/2),u=s((e-i)/2),d=a((e-i)/2),p=s((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}}});class vt{constructor(t=0,e=0){this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this)}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this)}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=tLine truncated
</script>
<script>
'use strict';
// ============================================================
// Scene Setup
// ============================================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a0f, 0.015);
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.setClearColor(0x0a0a0f);
document.body.prepend(renderer.domElement);
// ============================================================
// Materials
// ============================================================
const matGold = new THREE.MeshStandardMaterial({ color: 0xc9a96e, metalness: 0.95, roughness: 0.15 });
const matRoseGold = new THREE.MeshStandardMaterial({ color: 0xb76e5d, metalness: 0.9, roughness: 0.2 });
const matSteel = new THREE.MeshStandardMaterial({ color: 0x8899aa, metalness: 0.95, roughness: 0.1 });
const matDarkSteel = new THREE.MeshStandardMaterial({ color: 0x334455, metalness: 0.9, roughness: 0.25 });
const matRuby = new THREE.MeshStandardMaterial({ color: 0xcc1133, metalness: 0.1, roughness: 0.05, transparent: true, opacity: 0.85 });
const matSapphire = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0, roughness: 0, transmission: 0.95, thickness: 0.3, ior: 1.77 });
const matLume = new THREE.MeshStandardMaterial({ color: 0xaaffaa, emissive: 0x66ff66, emissiveIntensity: 0.6, metalness: 0, roughness: 0.8 });
const matBluedSteel = new THREE.MeshStandardMaterial({ color: 0x2244aa, metalness: 0.9, roughness: 0.15 });
const matBlack = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.3, roughness: 0.8 });
const matSpring = new THREE.MeshStandardMaterial({ color: 0x5566aa, metalness: 0.85, roughness: 0.2 });
// ============================================================
// Watch Group
// ============================================================
const watchGroup = new THREE.Group();
scene.add(watchGroup);
// ============================================================
// Helper: create gear
// ============================================================
function createGear(outerR, innerR, teeth, thickness, mat) {
const shape = new THREE.Shape();
const toothH = (outerR - innerR) * 0.5;
const baseR = outerR - toothH;
const step = (Math.PI * 2) / teeth;
for (let i = 0; i < teeth; i++) {
const a0 = i * step;
const a1 = a0 + step * 0.15;
const a2 = a0 + step * 0.35;
const a3 = a0 + step * 0.5;
const a4 = a0 + step * 0.65;
const a5 = a0 + step * 0.85;
if (i === 0) shape.moveTo(Math.cos(a0) * baseR, Math.sin(a0) * baseR);
shape.lineTo(Math.cos(a1) * baseR, Math.sin(a1) * baseR);
shape.lineTo(Math.cos(a2) * outerR, Math.sin(a2) * outerR);
shape.lineTo(Math.cos(a3) * outerR, Math.sin(a3) * outerR);
shape.lineTo(Math.cos(a4) * baseR, Math.sin(a4) * baseR);
shape.lineTo(Math.cos(a5) * baseR, Math.sin(a5) * baseR);
}
shape.closePath();
// Center hole
const hole = new THREE.Path();
for (let i = 0; i <= 32; i++) {
const a = (i / 32) * Math.PI * 2;
if (i === 0) hole.moveTo(Math.cos(a) * innerR, Math.sin(a) * innerR);
else hole.lineTo(Math.cos(a) * innerR, Math.sin(a) * innerR);
}
shape.holes.push(hole);
const geo = new THREE.ExtrudeGeometry(shape, { depth: thickness, bevelEnabled: false });
return new THREE.Mesh(geo, mat);
}
// ============================================================
// Helper: create bridge/plate
// ============================================================
function createBridge(width, height, depth, mat) {
const shape = new THREE.Shape();
const r = height / 2;
shape.moveTo(-width / 2 + r, -height / 2);
shape.lineTo(width / 2 - r, -height / 2);
shape.absarc(width / 2 - r, 0, r, -Math.PI / 2, Math.PI / 2, false);
shape.lineTo(-width / 2 + r, height / 2);
shape.absarc(-width / 2 + r, 0, r, Math.PI / 2, -Math.PI / 2, false);
// Screw holes
const addHole = (cx, cy, hr) => {
const h = new THREE.Path();
for (let i = 0; i <= 16; i++) {
const a = (i / 16) * Math.PI * 2;
if (i === 0) h.moveTo(cx + Math.cos(a) * hr, cy + Math.sin(a) * hr);
else h.lineTo(cx + Math.cos(a) * hr, cy + Math.sin(a) * hr);
}
shape.holes.push(h);
};
addHole(-width / 2 + r, 0, 0.03);
addHole(width / 2 - r, 0, 0.03);
const geo = new THREE.ExtrudeGeometry(shape, { depth: depth, bevelEnabled: false });
return new THREE.Mesh(geo, mat);
}
// ============================================================
// Mainspring Barrel
// ============================================================
const barrelGroup = new THREE.Group();
barrelGroup.position.set(-0.6, 0, 0.15);
watchGroup.add(barrelGroup);
const barrelBody = new THREE.Mesh(
new THREE.CylinderGeometry(0.35, 0.35, 0.08, 48),
matGold
);
barrelBody.rotation.x = Math.PI / 2;
barrelGroup.add(barrelBody);
const barrelCap = new THREE.Mesh(
new THREE.CylinderGeometry(0.33, 0.33, 0.01, 48),
matRoseGold
);
barrelCap.rotation.x = Math.PI / 2;
barrelCap.position.z = 0.045;
barrelGroup.add(barrelCap);
// Mainspring coil inside (visible through skeleton)
const springCurve = [];
for (let i = 0; i < 120; i++) {
const t = i / 120;
const r = 0.05 + t * 0.25;
const a = t * Math.PI * 8;
springCurve.push(new THREE.Vector3(Math.cos(a) * r, Math.sin(a) * r, 0));
}
const springGeo = new THREE.TubeGeometry(new THREE.CatmullRomCurve3(springCurve), 120, 0.008, 4, false);
const mainspring = new THREE.Mesh(springGeo, matSpring);
barrelGroup.add(mainspring);
// Barrel arbor
const arbor = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.03, 0.15, 16), matSteel);
arbor.rotation.x = Math.PI / 2;
barrelGroup.add(arbor);
// ============================================================
// Gear Train
// ============================================================
const gears = [];
const gearData = [
{ x: -0.15, y: 0, z: 0.12, r: 0.25, teeth: 40, speed: 0.1, mat: matGold, thickness: 0.015 },
{ x: 0.2, y: 0.15, z: 0.12, r: 0.15, teeth: 24, speed: -0.2, mat: matRoseGold, thickness: 0.012 },
{ x: 0.2, y: -0.15, z: 0.12, r: 0.18, teeth: 30, speed: 0.15, mat: matGold, thickness: 0.013 },
{ x: 0.55, y: 0, z: 0.12, r: 0.22, teeth: 36, speed: -0.08, mat: matSteel, thickness: 0.014 },
{ x: 0.55, y: 0, z: 0.06, r: 0.1, teeth: 16, speed: 0.3, mat: matBluedSteel, thickness: 0.01 },
];
gearData.forEach(d => {
const gear = createGear(d.r, d.r * 0.15, d.teeth, d.thickness, d.mat);
gear.position.set(d.x, d.y, d.z);
gear.userData = { speed: d.speed, baseZ: d.z, label: `Gear (${d.teeth}T)` };
watchGroup.add(gear);
gears.push(gear);
// Axle
const axle = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, 0.2, 8), matSteel);
axle.rotation.x = Math.PI / 2;
axle.position.set(d.x, d.y, d.z);
watchGroup.add(axle);
});
// ============================================================
// Escapement
// ============================================================
const escapementGroup = new THREE.Group();
escapementGroup.position.set(0.7, 0, 0.15);
watchGroup.add(escapementGroup);
// Escape wheel
const escapeWheel = createGear(0.12, 0.02, 20, 0.008, matBluedSteel);
escapementGroup.add(escapeWheel);
escapementGroup.userData = { speed: 1.5, label: 'Escape Wheel' };
// Pallet fork
const palletFork = new THREE.Group();
const forkBody = new THREE.Mesh(
new THREE.BoxGeometry(0.02, 0.15, 0.006),
matSteel
);
palletFork.add(forkBody);
// Pallet stones
const palletStone1 = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.03, 0.008), matRuby);
palletStone1.position.set(0, 0.07, 0);
palletFork.add(palletStone1);
const palletStone2 = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.03, 0.008), matRuby);
palletStone2.position.set(0, -0.07, 0);
palletFork.add(palletStone2);
// Fork end
const forkEnd = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.015, 0.006), matSteel);
forkEnd.position.set(0, -0.09, 0);
palletFork.add(forkEnd);
palletFork.position.set(0.7, 0, 0.18);
palletFork.userData = { label: 'Pallet Fork' };
watchGroup.add(palletFork);
// ============================================================
// Balance Wheel with Hairspring
// ============================================================
const balanceGroup = new THREE.Group();
balanceGroup.position.set(0.7, -0.35, 0.15);
watchGroup.add(balanceGroup);
// Balance wheel rim
const balanceRim = new THREE.Mesh(
new THREE.TorusGeometry(0.13, 0.008, 8, 48),
matGold
);
balanceGroup.add(balanceRim);
// Balance arms (crossbar)
for (let i = 0; i < 2; i++) {
const arm = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.012, 0.008), matGold);
arm.rotation.z = i * Math.PI / 2;
balanceGroup.add(arm);
}
// Timing screws on rim
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2;
const screw = new THREE.Mesh(new THREE.SphereGeometry(0.006, 8, 8), matRoseGold);
screw.position.set(Math.cos(a) * 0.13, Math.sin(a) * 0.13, 0.008);
balanceGroup.add(screw);
}
// Hairspring (Breguet overcoil)
const hairspringPoints = [];
for (let i = 0; i < 200; i++) {
const t = i / 200;
const r = 0.02 + t * 0.09;
const a = t * Math.PI * 14;
hairspringPoints.push(new THREE.Vector3(Math.cos(a) * r, Math.sin(a) * r, t * 0.015));
}
const hairspringGeo = new THREE.TubeGeometry(new THREE.CatmullRomCurve3(hairspringPoints), 200, 0.0015, 4, false);
const hairspring = new THREE.Mesh(hairspringGeo, matBluedSteel);
hairspring.userData = { label: 'Hairspring (Breguet)' };
balanceGroup.add(hairspring);
// Impulse pin
const impulsePin = new THREE.Mesh(new THREE.CylinderGeometry(0.004, 0.004, 0.02, 8), matRuby);
impulsePin.position.set(0, 0.06, 0.008);
balanceGroup.add(impulsePin);
// ============================================================
// Bridges / Plates
// ============================================================
// Main bridge (covers gear train)
const mainBridge = createBridge(0.9, 0.25, 0.02, matRoseGold);
mainBridge.position.set(0.2, 0, 0.04);
mainBridge.userData = { label: 'Main Bridge (Côtes de Genève)' };
watchGroup.add(mainBridge);
// Balance cock
const balanceCock = createBridge(0.35, 0.2, 0.015, matGold);
balanceCock.position.set(0.7, -0.35, 0.04);
balanceCock.userData = { label: 'Balance Cock' };
watchGroup.add(balanceCock);
// Barrel bridge
const barrelBridge = createBridge(0.5, 0.3, 0.015, matRoseGold);
barrelBridge.position.set(-0.6, 0, 0.04);
barrelBridge.userData = { label: 'Barrel Bridge' };
watchGroup.add(barrelBridge);
// Base plate
const basePlate = new THREE.Mesh(
new THREE.CylinderGeometry(1.0, 1.0, 0.015, 64),
matDarkSteel
);
basePlate.rotation.x = Math.PI / 2;
basePlate.position.z = 0.01;
watchGroup.add(basePlate);
// ============================================================
// Ruby Jewels
// ============================================================
const jewels = [];
const jewelPositions = [
[-0.6, 0, 0.2], [0.2, 0, 0.2], [0.2, 0.15, 0.2], [0.2, -0.15, 0.2],
[0.55, 0, 0.2], [0.7, 0, 0.2], [0.7, -0.35, 0.2],
[-0.6, 0, 0.03], [0.2, 0, 0.03], [0.55, 0, 0.03], [0.7, 0, 0.03],
];
jewelPositions.forEach(([x, y, z]) => {
const jewel = new THREE.Mesh(new THREE.CylinderGeometry(0.018, 0.018, 0.008, 16), matRuby);
jewel.rotation.x = Math.PI / 2;
jewel.position.set(x, y, z);
jewel.userData = { label: 'Ruby Jewel Bearing' };
watchGroup.add(jewel);
jewels.push(jewel);
});
// ============================================================
// Watch Hands
// ============================================================
const handsGroup = new THREE.Group();
handsGroup.position.z = 0.25;
watchGroup.add(handsGroup);
function createHand(length, width, mat) {
const shape = new THREE.Shape();
shape.moveTo(-width / 2, 0);
shape.lineTo(-width / 3, length);
shape.lineTo(0, length + 0.01);
shape.lineTo(width / 3, length);
shape.lineTo(width / 2, 0);
shape.lineTo(width / 2, -length * 0.15);
shape.lineTo(-width / 2, -length * 0.15);
shape.closePath();
return new THREE.ExtrudeGeometry(shape, { depth: 0.008, bevelEnabled: false });
}
const hourHand = new THREE.Mesh(createHand(0.35, 0.04, matLume), matGold);
hourHand.position.z = 0.01;
hourHand.userData = { label: 'Hour Hand (Luminous)' };
handsGroup.add(hourHand);
const minuteHand = new THREE.Mesh(createHand(0.5, 0.03, matLume), matSteel);
minuteHand.position.z = 0.02;
minuteHand.userData = { label: 'Minute Hand (Luminous)' };
handsGroup.add(minuteHand);
const secondHand = new THREE.Mesh(
new THREE.CylinderGeometry(0.003, 0.003, 0.55, 4),
matRoseGold
);
secondHand.rotation.x = Math.PI / 2;
secondHand.position.set(0, 0.12, 0.03);
secondHand.position.z = 0.03;
secondHand.userData = { label: 'Seconds Hand' };
handsGroup.add(secondHand);
// Center jewel
const centerJewel = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.015, 16), matRuby);
centerJewel.rotation.x = Math.PI / 2;
centerJewel.position.z = 0.035;
handsGroup.add(centerJewel);
// ============================================================
// Hour Markers (on dial ring)
// ============================================================
for (let i = 0; i < 12; i++) {
const a = (i / 12) * Math.PI * 2 - Math.PI / 2;
const r = 0.82;
const isMain = i % 3 === 0;
const marker = new THREE.Mesh(
new THREE.BoxGeometry(isMain ? 0.04 : 0.02, isMain ? 0.1 : 0.06, 0.015),
isMain ? matGold : matSteel
);
marker.position.set(Math.cos(a) * r, Math.sin(a) * r, 0.22);
marker.rotation.z = a + Math.PI / 2;
watchGroup.add(marker);
if (isMain) {
// Lume dot
const dot = new THREE.Mesh(new THREE.SphereGeometry(0.01, 8, 8), matLume);
dot.position.set(Math.cos(a) * (r - 0.07), Math.sin(a) * (r - 0.07), 0.23);
watchGroup.add(dot);
}
}
// Minute track
for (let i = 0; i < 60; i++) {
const a = (i / 60) * Math.PI * 2;
const r = 0.88;
const tick = new THREE.Mesh(
new THREE.BoxGeometry(0.005, i % 5 === 0 ? 0.04 : 0.02, 0.005),
matSteel
);
tick.position.set(Math.cos(a) * r, Math.sin(a) * r, 0.22);
tick.rotation.z = a + Math.PI / 2;
watchGroup.add(tick);
}
// ============================================================
// Watch Case
// ============================================================
// Case band
const caseBand = new THREE.Mesh(
new THREE.TorusGeometry(0.95, 0.08, 16, 64),
matSteel
);
caseBand.position.z = 0.12;
watchGroup.add(caseBand);
// Case back (transparent sapphire)
const caseBack = new THREE.Mesh(
new THREE.CircleGeometry(0.93, 64),
matSapphire
);
caseBack.position.z = 0.0;
watchGroup.add(caseBack);
// Case front (sapphire crystal)
const caseFront = new THREE.Mesh(
new THREE.CircleGeometry(0.93, 64),
matSapphire
);
caseFront.position.z = 0.26;
watchGroup.add(caseFront);
// Bezel
const bezel = new THREE.Mesh(
new THREE.TorusGeometry(0.93, 0.03, 12, 64),
matRoseGold
);
bezel.position.z = 0.25;
watchGroup.add(bezel);
// Crown
const crown = new THREE.Mesh(
new THREE.CylinderGeometry(0.05, 0.06, 0.12, 16),
matRoseGold
);
crown.rotation.z = Math.PI / 2;
crown.position.set(1.08, 0, 0.12);
watchGroup.add(crown);
// Crown ridges
for (let i = 0; i < 12; i++) {
const a = (i / 12) * Math.PI * 2;
const ridge = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.004, 0.004), matGold);
ridge.position.set(1.08, Math.cos(a) * 0.055, 0.12 + Math.sin(a) * 0.055);
ridge.rotation.x = a;
watchGroup.add(ridge);
}
// Lugs
const lugShape = (side) => {
const lug = new THREE.Mesh(
new THREE.BoxGeometry(0.08, 0.25, 0.06),
matSteel
);
lug.position.set(side * 0.35, side > 0 ? 0.95 : -0.95, 0.12);
lug.rotation.z = side > 0 ? 0.15 : -0.15;
return lug;
};
watchGroup.add(lugShape(1));
watchGroup.add(lugShape(-1));
const lug2Shape = (side) => {
const lug = new THREE.Mesh(
new THREE.BoxGeometry(0.08, 0.25, 0.06),
matSteel
);
lug.position.set(side * 0.85, side > 0 ? 0.7 : -0.7, 0.12);
lug.rotation.z = side > 0 ? -0.15 : 0.15;
return lug;
};
watchGroup.add(lug2Shape(1));
watchGroup.add(lug2Shape(-1));
// ============================================================
// Lighting
// ============================================================
const ambientLight = new THREE.AmbientLight(0x334455, 0.4);
scene.add(ambientLight);
// Key light
const keyLight = new THREE.SpotLight(0xfff5e6, 2.0);
keyLight.position.set(3, 4, 5);
keyLight.angle = Math.PI / 6;
keyLight.penumbra = 0.5;
keyLight.decay = 2;
keyLight.distance = 20;
keyLight.castShadow = true;
keyLight.shadow.mapSize.set(2048, 2048);
scene.add(keyLight);
// Fill light
const fillLight = new THREE.SpotLight(0x8899cc, 0.8);
fillLight.position.set(-3, 2, 3);
fillLight.angle = Math.PI / 4;
fillLight.penumbra = 0.8;
scene.add(fillLight);
// Rim light
const rimLight = new THREE.SpotLight(0xc9a96e, 1.2);
rimLight.position.set(0, -2, -3);
rimLight.angle = Math.PI / 5;
rimLight.penumbra = 0.6;
scene.add(rimLight);
// Top light for movement
const topLight = new THREE.PointLight(0xffffff, 0.6, 10);
topLight.position.set(0, 0, 3);
scene.add(topLight);
// ============================================================
// Environment (simple gradient backdrop)
// ============================================================
const bgGeo = new THREE.SphereGeometry(30, 32, 32);
const bgMat = new THREE.MeshBasicMaterial({ color: 0x0a0a0f, side: THREE.BackSide });
scene.add(new THREE.Mesh(bgGeo, bgMat));
// Reflective floor
const floorGeo = new THREE.PlaneGeometry(20, 20);
const floorMat = new THREE.MeshStandardMaterial({
color: 0x111115, metalness: 0.8, roughness: 0.3,
});
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.position.y = -2;
floor.receiveShadow = true;
scene.add(floor);
// ============================================================
// OrbitControls (inline minimal implementation)
// ============================================================
let isDragging = false;
let prevMouse = { x: 0, y: 0 };
let spherical = { theta: 0, phi: Math.PI / 3, radius: 5 };
let targetSpherical = { ...spherical };
renderer.domElement.addEventListener('pointerdown', e => { isDragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
renderer.domElement.addEventListener('pointermove', e => {
if (!isDragging) return;
const dx = e.clientX - prevMouse.x;
const dy = e.clientY - prevMouse.y;
targetSpherical.theta -= dx * 0.005;
targetSpherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, targetSpherical.phi - dy * 0.005));
prevMouse = { x: e.clientX, y: e.clientY };
});
renderer.domElement.addEventListener('pointerup', () => { isDragging = false; });
renderer.domElement.addEventListener('wheel', e => {
targetSpherical.radius = Math.max(2, Math.min(10, targetSpherical.radius + e.deltaY * 0.005));
});
function updateCamera() {
spherical.theta += (targetSpherical.theta - spherical.theta) * 0.08;
spherical.phi += (targetSpherical.phi - spherical.phi) * 0.08;
spherical.radius += (targetSpherical.radius - spherical.radius) * 0.08;
camera.position.x = spherical.radius * Math.sin(spherical.phi) * Math.sin(spherical.theta);
camera.position.y = spherical.radius * Math.cos(spherical.phi);
camera.position.z = spherical.radius * Math.sin(spherical.phi) * Math.cos(spherical.theta);
camera.lookAt(0, 0, 0.12);
}
// ============================================================
// Animation State
// ============================================================
let isExploded = false;
let isPaused = false;
let showInfo = false;
let animTime = 0;
const clock = new THREE.Clock();
// Exploded view positions
const originalPositions = new Map();
const explodedOffsets = new Map();
function storeOriginalPositions() {
watchGroup.children.forEach(child => {
originalPositions.set(child.uuid, child.position.clone());
const dir = child.position.clone().normalize();
if (dir.length() < 0.01) dir.set(0, 0, 1);
explodedOffsets.set(child.uuid, dir.multiplyScalar(0.5));
});
}
storeOriginalPositions();
function toggleExplode() {
isExploded = !isExploded;
document.getElementById('btn-explode').classList.toggle('active', isExploded);
}
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
if (!isPaused) animTime += dt;
// Update camera
updateCamera();
// Animate gears
gears.forEach(g => {
g.rotation.z += g.userData.speed * dt * 2;
});
// Animate escape wheel
escapeWheel.rotation.z += 1.5 * dt * 2;
// Animate balance wheel (oscillation)
balanceGroup.rotation.z = Math.sin(animTime * 8) * 0.5;
// Animate pallet fork (ticking)
palletFork.rotation.z = Math.sin(animTime * 8) * 0.15;
// Animate hands
const now = new Date();
const s = now.getSeconds() + now.getMilliseconds() / 1000;
const m = now.getMinutes() + s / 60;
const h = (now.getHours() % 12) + m / 60;
secondHand.rotation.z = -s * (Math.PI / 30);
minuteHand.rotation.z = -m * (Math.PI / 30);
hourHand.rotation.z = -h * (Math.PI / 6);
// Update clock display
const pad = n => String(n).padStart(2, '0');
document.getElementById('clock-display').textContent =
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(s.toFixed(0))}`;
// Explode animation
watchGroup.children.forEach(child => {
const orig = originalPositions.get(child.uuid);
const offset = explodedOffsets.get(child.uuid);
if (!orig || !offset) return;
const target = isExploded ? orig.clone().add(offset) : orig;
child.position.lerp(target, 0.05);
});
renderer.render(scene, camera);
}
// ============================================================
// Interaction: hover labels
// ============================================================
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const tooltip = document.getElementById('label-tooltip');
renderer.domElement.addEventListener('mousemove', e => {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(watchGroup.children, true);
if (intersects.length > 0) {
let obj = intersects[0].object;
while (obj && !obj.userData.label) obj = obj.parent;
if (obj && obj.userData.label) {
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX + 12) + 'px';
tooltip.style.top = (e.clientY - 8) + 'px';
tooltip.textContent = obj.userData.label;
return;
}
}
tooltip.style.display = 'none';
});
// ============================================================
// Controls
// ============================================================
document.getElementById('btn-explode').addEventListener('click', toggleExplode);
document.getElementById('btn-info').addEventListener('click', () => {
showInfo = !showInfo;
document.getElementById('info-panel').style.display = showInfo ? 'block' : 'none';
document.getElementById('btn-info').classList.toggle('active', showInfo);
});
document.getElementById('btn-pause').addEventListener('click', () => {
isPaused = !isPaused;
document.getElementById('btn-pause').classList.toggle('active', isPaused);
document.getElementById('btn-pause').textContent = isPaused ? 'PLAY' : 'PAUSE';
});
document.addEventListener('keydown', e => {
if (e.code === 'KeyE') toggleExplode();
if (e.code === 'KeyI') {
showInfo = !showInfo;
document.getElementById('info-panel').style.display = showInfo ? 'block' : 'none';
document.getElementById('btn-info').classList.toggle('active', showInfo);
}
if (e.code === 'Space') {
e.preventDefault();
isPaused = !isPaused;
document.getElementById('btn-pause').classList.toggle('active', isPaused);
document.getElementById('btn-pause').textContent = isPaused ? 'PLAY' : 'PAUSE';
}
});
// ============================================================
// Pointer lock & start
// ============================================================
const blocker = document.getElementById('blocker');
document.getElementById('startBtn').addEventListener('click', () => {
blocker.style.display = 'none';
document.getElementById('clock-display').style.display = 'block';
});
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement !== document.body) {
// Don't show blocker on pointer unlock - use UI buttons instead
}
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start
animate();
</script>
</body>
</html>