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,628 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mythos Craft - 3D Voxel World</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#000;font-family:'Courier New',monospace;color:#fff}
canvas{display:block}
#blocker{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:100;cursor:pointer}
#blocker .inner{text-align:center;max-width:600px;padding:40px}
#blocker h1{font-size:48px;color:#4fc3f7;text-shadow:0 0 20px #4fc3f7;margin-bottom:10px}
#blocker h2{font-size:18px;color:#aaa;margin-bottom:30px}
#blocker .controls{text-align:left;background:rgba(255,255,255,0.05);border-radius:12px;padding:24px;margin-bottom:30px;line-height:2}
#blocker .controls span{color:#4fc3f7;font-weight:bold}
#blocker .start-btn{display:inline-block;padding:14px 48px;background:linear-gradient(135deg,#4fc3f7,#0288d1);color:#000;font-size:20px;font-weight:bold;border:none;border-radius:8px;cursor:pointer;transition:transform 0.2s}
#blocker .start-btn:hover{transform:scale(1.05)}
#crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:50;pointer-events:none}
#crosshair::before,#crosshair::after{content:'';position:absolute;background:rgba(255,255,255,0.8)}
#crosshair::before{width:20px;height:2px;top:-1px;left:-10px}
#crosshair::after{width:2px;height:20px;top:-10px;left:-1px}
#hotbar{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);display:flex;gap:4px;z-index:50;pointer-events:none}
.slot{width:52px;height:52px;border:2px solid rgba(255,255,255,0.3);border-radius:6px;display:flex;align-items:center;justify-content:center;position:relative;background:rgba(0,0,0,0.5)}
.slot.active{border-color:#4fc3f7;box-shadow:0 0 10px #4fc3f7}
.slot canvas{width:36px;height:36px;border-radius:3px}
.slot .num{position:absolute;top:2px;left:4px;font-size:10px;color:#aaa}
.slot .bname{position:absolute;bottom:-18px;font-size:9px;color:#aaa;white-space:nowrap;left:50%;transform:translateX(-50%)}
#hud{position:fixed;top:16px;left:16px;z-index:50;font-size:13px;color:rgba(255,255,255,0.7);pointer-events:none;line-height:1.8}
#info{position:fixed;bottom:16px;left:16px;z-index:50;font-size:12px;color:rgba(255,255,255,0.5);pointer-events:none;line-height:1.6}
</style>
</head>
<body>
<div id="blocker">
<div class="inner">
<h1>Mythos Craft</h1>
<h2>神话境界 - A Mythic Fantasy Voxel World</h2>
<div class="controls">
<span>WASD</span> - 移动 Move<br>
<span>鼠标</span> - 环视 Look Around<br>
<span>空格</span> - 跳跃 Jump<br>
<span>左键</span> - 破坏方块 Break Block<br>
<span>右键</span> - 放置方块 Place Block<br>
<span>1-9 / 滚轮</span> - 选择方块 Select Block<br>
<span>F</span> - 飞行模式 Toggle Fly
</div>
<button class="start-btn" id="startBtn">点击进入 Click to Start</button>
</div>
</div>
<div id="crosshair"></div>
<div id="hotbar"></div>
<div id="hud"></div>
<div id="info"></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';
// ============================================================
// Perlin Noise (classic 2D)
// ============================================================
const perm=[151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
const p=new Array(512);const g=new Array(512);
for(let i=0;i<512;i++){p[i]=perm[i&255];g[i]=perm[i&255]%12}
const grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
function dot(gi,x,y){const v=grad3[gi];return v[0]*x+v[1]*y}
function noise(xin,yin){const F2=0.5*(Math.sqrt(3)-1);const G2=(3-Math.sqrt(3))/6;const s=(xin+yin)*F2;const i=Math.floor(xin+s);const j=Math.floor(yin+s);const t=(i+j)*G2;const X0=i-t;const Y0=j-t;const x0=xin-X0;const y0=yin-Y0;let i1,j1;if(x0>y0){i1=1;j1=0}else{i1=0;j1=1}const x1=x0-i1+G2;const y1=y0-j1+G2;const x2=x0-1+2*G2;const y2=y0-1+2*G2;const ii=i&255;const jj=j&255;const gi0=g[ii+p[jj]&511];const gi1=g[ii+i1+p[jj+j1]&511];const gi2=g[ii+1+p[jj+1]&511];let n0,n1,n2;let t0=0.5-x0*x0-y0*y0;if(t0<0)n0=0;else{t0*=t0;n0=t0*t0*dot(gi0,x0,y0)}let t1=0.5-x1*x1-y1*y1;if(t1<0)n1=0;else{t1*=t1;n1=t1*t1*dot(gi1,x1,y1)}let t2=0.5-x2*x2-y2*y2;if(t2<0)n2=0;else{t2*=t2;n2=t2*t2*dot(gi2,x2,y2)}return 70*(n0+n1+n2)}
function fbm(x,y,octaves=6,lacunarity=2,gain=0.5){let v=0,a=1,f=1,t=0;for(let i=0;i<octaves;i++){v+=a*noise(x*f,y*f);t+=a;a*=gain;f*=lacunarity}return v/t}
function getTerrainHeight(wx,wz){const s=0.008;let h=fbm(wx*s+5.3,wz*s+9.1,6,2,0.5)*30+25;const ridge=Math.abs(fbm(wx*0.003+100,wz*0.003+200,4,2,0.5))*25;h+=ridge;const mountain=fbm(wx*0.005+50,wz*0.005+50,3,2,0.5);if(mountain>0.2){h+=(mountain-0.2)*60}return Math.floor(h)}
// ============================================================
// Block Types
// ============================================================
const B={AIR:0,DIRT:1,GRASS:2,STONE:3,SAND:4,WATER:5,WOOD:6,LEAVES:7,BRICK:8,Obsidian:9,Crystal:10,Gold:11,Glowstone:12,Bone:13,Amethyst:14,DarkStone:15};
const BNAME={[B.DIRT]:'Dirt',[B.GRASS]:'Grass',[B.STONE]:'Stone',[B.SAND]:'Sand',[B.WATER]:'Water',[B.WOOD]:'Wood',[B.LEAVES]:'Leaves',[B.BRICK]:'Brick',[B.Obsidian]:'Obsidian',[B.Crystal]:'Crystal',[B.Gold]:'Gold',[B.Glowstone]:'Glow',[B.Bone]:'Bone',[B.Amethyst]:'Amethyst',[B.DarkStone]:'DkStone'};
const BCOLOR={
[B.DIRT]:{top:'#8B6914',side:'#6B4400',bottom:'#5a3800'},
[B.GRASS]:{top:'#228B22',side:'#6B4400',bottom:'#5a3800',sideTop:'#32CD32'},
[B.STONE]:{top:'#808080',side:'#707070',bottom:'#606060'},
[B.SAND]:{top:'#C2B280',side:'#B8A870',bottom:'#A89860'},
[B.WATER]:{top:'#1E90FF',side:'#1873CC',bottom:'#1060B0'},
[B.WOOD]:{top:'#8B4513',side:'#654321',bottom:'#8B4513'},
[B.LEAVES]:{top:'#0B6623',side:'#0A5520',bottom:'#094418'},
[B.BRICK]:{top:'#8B0000',side:'#A00000',bottom:'#700000'},
[B.Obsidian]:{top:'#1a0033',side:'#120022',bottom:'#0a0015'},
[B.Crystal]:{top:'#88CCFF',side:'#66AAEE',bottom:'#4488CC'},
[B.Gold]:{top:'#FFD700',side:'#DAA520',bottom:'#B8860B'},
[B.Glowstone]:{top:'#FFA500',side:'#FF8C00',bottom:'#CC7000'},
[B.Bone]:{top:'#F5F5DC',side:'#E8E0C8',bottom:'#D8CEB0'},
[B.Amethyst]:{top:'#9966CC',side:'#8855BB',bottom:'#7744AA'},
[B.DarkStone]:{top:'#333340',side:'#282830',bottom:'#1e1e25'}
};
const HOTBAR_BLOCKS=[B.GRASS,B.DIRT,B.STONE,B.WOOD,B.BRICK,B.Crystal,B.Gold,B.Glowstone,B.Bone];
// ============================================================
// Texture Generation (canvas-based per block type)
// ============================================================
function makeTex(hex,fn){const c=document.createElement('canvas');c.width=c.height=16;const x=c.getContext('2d');x.fillStyle=hex;x.fillRect(0,0,16,16);if(fn)fn(x);return c}
function noiseFill(basehex,ranges){const c=document.createElement('canvas');c.width=c.height=16;const x=c.getContext('2d');x.fillStyle=basehex;x.fillRect(0,0,16,16);const id=x.getImageData(0,0,16,16);for(let i=0;i<id.data.length;i+=4){const n=(Math.random()-0.5)*ranges;id.data[i]=Math.min(255,Math.max(0,id.data[i]+n));id.data[i+1]=Math.min(255,Math.max(0,id.data[i+1]+n));id.data[i+2]=Math.min(255,Math.max(0,id.data[i+2]+n))}x.putImageData(id,0,0);return c}
const textures={};
function genTextures(){
textures.dirt=noiseFill('#8B6914',25);
textures.stone=noiseFill('#808080',20);
textures.sand=noiseFill('#C2B280',15);
textures.wood_top=makeTex('#8B4513',c=>{c.strokeStyle='#654321';c.lineWidth=1;for(let i=2;i<16;i+=3){c.beginPath();c.arc(8,8,i,0,Math.PI*2);c.stroke()}});
textures.wood_side=makeTex('#654321',c=>{c.strokeStyle='#4a3010';c.lineWidth=1;for(let y=0;y<16;y+=4){c.beginPath();c.moveTo(0,y+2);c.bezierCurveTo(4,y,12,y+4,16,y+2);c.stroke()}});
textures.leaves=noiseFill('#0B6623',30);
textures.brick=makeTex('#8B0000',c=>{c.fillStyle='#d4a574';c.fillRect(0,0,16,16);c.fillStyle='#8B0000';for(let r=0;r<4;r++){const y=r*4;const off=r%2===1?4:0;for(let bx=-1;bx<4;bx++){c.fillRect(bx*8+off,y,7,3)}}});
textures.obsidian=noiseFill('#1a0033',12);
textures.crystal=makeTex('#88CCFF',c=>{c.globalAlpha=0.3;for(let i=0;i<8;i++){c.fillStyle=i%2?'#aaddff':'#6699cc';c.fillRect(Math.random()*12,Math.random()*12,3,3)}c.globalAlpha=1;c.strokeStyle='#ffffff44';c.lineWidth=0.5;c.strokeRect(2,2,12,12)});
textures.gold=makeTex('#FFD700',c=>{c.globalAlpha=0.2;for(let i=0;i<6;i++){c.fillStyle='#fff';c.fillRect(Math.random()*14,Math.random()*14,2,2)}c.globalAlpha=1});
textures.glowstone=makeTex('#FFA500',c=>{c.globalAlpha=0.4;for(let i=0;i<10;i++){c.fillStyle='#ffee88';const s=1+Math.random()*3;c.fillRect(Math.random()*14,Math.random()*14,s,s)}c.globalAlpha=1});
textures.bone=makeTex('#F5F5DC',c=>{c.strokeStyle='#d0c8a0';c.lineWidth=0.5;for(let i=0;i<5;i++){c.beginPath();c.moveTo(Math.random()*16,Math.random()*16);c.lineTo(Math.random()*16,Math.random()*16);c.stroke()}});
textures.amethyst=makeTex('#9966CC',c=>{c.globalAlpha=0.3;for(let i=0;i<8;i++){c.fillStyle=i%2?'#bb88ee':'#7744aa';c.fillRect(Math.random()*12,Math.random()*12,3,3)}c.globalAlpha=1});
textures.darkstone=noiseFill('#333340',15);
textures.grass_top=noiseFill('#228B22',25);
textures.grass_side=makeTex('#6B4400',c=>{
c.fillStyle='#6B4400';c.fillRect(0,0,16,16);
const id=c.getImageData(0,0,16,16);for(let i=0;i<id.data.length;i+=4){const n=(Math.random()-0.5)*20;id.data[i]=Math.min(255,Math.max(0,id.data[i]+n));id.data[i+1]=Math.min(255,Math.max(0,id.data[i+1]+n));id.data[i+2]=Math.min(255,Math.max(0,id.data[i+2]+n))}
c.putImageData(id,0,0);
c.fillStyle='#32CD32';for(let x=0;x<16;x++){const h=2+Math.floor(Math.random()*3);c.fillRect(x,0,1,h)}
});
textures.water=makeTex('#1E90FF',c=>{c.globalAlpha=0.6;c.fillStyle='#4db8ff';for(let i=0;i<4;i++){c.fillRect(0,i*4+1,16,1)}c.globalAlpha=1});
}
function getTexName(blockType,face){
if(blockType===B.GRASS){if(face===1)return'textures.grass_top';if(face===0||face===2||face===3||face===4||face===5)return'textures.grass_side'}
if(blockType===B.WOOD){if(face===1||face===0)return'textures.wood_top';return'textures.wood_side'}
const map={1:'dirt',3:'stone',4:'sand',5:'water',7:'leaves',8:'brick',9:'obsidian',10:'crystal',11:'gold',12:'glowstone',13:'bone',14:'amethyst',15:'darkstone'};
return'textures.'+(map[blockType]||'stone');
}
// ============================================================
// Chunk
// ============================================================
const CS=16,CH=64;
const DIRS=[{dx:1,dz:0},{dx:-1,dz:0},{dx:0,dz:1},{dx:0,dz:-1}];
const FACE_VERTS=[
[[1,0,0],[1,1,0],[1,1,1],[1,0,1]],
[[0,0,1],[0,1,1],[0,1,0],[0,0,0]],
[[0,1,1],[1,1,1],[1,1,0],[0,1,0]],
[[0,0,0],[1,0,0],[1,0,1],[0,0,1]],
[[0,0,1],[1,0,1],[1,1,1],[0,1,1]],
[[1,0,0],[0,0,0],[0,1,0],[1,1,0]]
];
const FACE_UVS=[[0,0],[0,1],[1,1],[1,0]];
const FACE_NORMALS=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
class Chunk{
constructor(cx,cz){
this.cx=cx;this.cz=cz;this.blocks=new Uint8Array(CS*CS*CH);this.mesh=null;this.dirty=true;this.generated=false;
}
idx(x,y,z){return x+z*CS+y*CS*CS}
get(x,y,z){if(x<0||x>=CS||y<0||y>=CH||z<0||z>=CS)return B.AIR;return this.blocks[this.idx(x,y,z)]}
set(x,y,z,t){if(x<0||x>=CS||y<0||y>=CH||z<0||z>=CS)return;this.blocks[this.idx(x,y,z)]=t;this.dirty=true}
generate(){
if(this.generated)return;this.generated=true;
for(let x=0;x<CS;x++){for(let z=0;z<CS;z++){
const wx=this.cx*CS+x,wz=this.cz*CS+z;
const h=getTerrainHeight(wx,wz);
for(let y=0;y<CH;y++){
let b=B.AIR;
if(y===0)b=B.Stone;
else if(y<h-4)b=B.Stone;
else if(y<h)b=B.DIRT;
else if(y===h){b=h>20?B.GRASS:B.SAND}
else if(y<=20&&y>h)b=B.WATER;
this.blocks[this.idx(x,y,z)]=b;
}
}}
}
getBlock(wx,wy,wz){
const lx=wx-this.cx*CS,lz=wz-this.cz*CS;
if(lx<0||lx>=CS||lz<0||lz>=CS||wy<0||wy>=CH)return B.AIR;
return this.blocks[this.idx(lx,wy,lz)];
}
setBlock(wx,wy,wz,t){
const lx=wx-this.cx*CS,lz=wz-this.cz*CS;
if(lx<0||lx>=CS||lz<0||lz>=CS||wy<0||wy>=CH)return;
this.blocks[this.idx(lx,wy,lz)]=t;this.dirty=true;
}
buildMesh(world){
if(!this.dirty)return;this.dirty=false;
if(this.mesh){this.mesh.geometry.dispose();this.mesh.material.dispose();this.mesh.parent?.remove(this.mesh)}
const pos=[],norms=[],uvs=[],cols=[],inds=[];let vi=0;
const atlW=4,atlH=4;
for(let x=0;x<CS;x++){for(let z=0;z<CS;z++){for(let y=0;y<CH;y++){
const bt=this.get(x,y,z);if(bt===B.AIR)continue;
for(let f=0;f<6;f++){
const nx=x+FACE_NORMALS[f][0],ny=y+FACE_NORMALS[f][1],nz=z+FACE_NORMALS[f][2];
let nb;
if(nx<0||nx>=CS||nz<0||nz>=CS){
const wbx=this.cx*CS+nx,wbz=this.cz*CS+nz;
nb=world.getBlock(wbx,ny,wbz);
}else if(ny<0||ny>=CH){nb=B.AIR}
else{nb=this.get(nx,ny,nz)}
if(nb!==B.AIR)continue;
const texName=getTexName(bt,f);
const ti=parseInt(texName.split('.')[1]);
const col=BCOLOR[bt];
const faceCol=f===1?col.top:f===0?col.bottom:col.side;
const r=parseInt(faceCol.slice(1,3),16)/255;
const g=parseInt(faceCol.slice(3,5),16)/255;
const b_=parseInt(faceCol.slice(5,7),16)/255;
const tx=ti%atlW,ty=Math.floor(ti/atlW);
const fu=tx/atlW,fv=ty/atlH,fuW=1/atlW,fvH=1/atlH;
for(let v=0;v<4;v++){
const vt=FACE_VERTS[f][v];
pos.push(x+vt[0],y+vt[1],z+vt[2]);
norms.push(FACE_NORMALS[f][0],FACE_NORMALS[f][1],FACE_NORMALS[f][2]);
uvs.push(fu+FACE_UVS[v][0]*fuW,fv+FACE_UVS[v][1]*fvH);
cols.push(r,g,b_);
}
inds.push(vi,vi+1,vi+2,vi,vi+2,vi+3);vi+=4;
}
}}}
if(pos.length===0)return;
const geo=new THREE.BufferGeometry();
geo.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));
geo.setAttribute('normal',new THREE.Float32BufferAttribute(norms,3));
geo.setAttribute('uv',new THREE.Float32BufferAttribute(uvs,2));
geo.setAttribute('color',new THREE.Float32BufferAttribute(cols,3));
geo.setIndex(inds);
const mat=new THREE.MeshStandardMaterial({map:atlas,vertexColors:true,side:THREE.FrontSide});
this.mesh=new THREE.Mesh(geo,mat);
this.mesh.position.set(this.cx*CS,0,this.cz*CS);
world.scene.add(this.mesh);
}
}
// ============================================================
// World
// ============================================================
class World{
constructor(scene){this.scene=scene;this.chunks=new Map();this.structures=new Set()}
key(cx,cz){return cx+','+cz}
getChunk(cx,cz){return this.chunks.get(this.key(cx,cz))}
ensureChunk(cx,cz){
let ch=this.getChunk(cx,cz);
if(!ch){ch=new Chunk(cx,cz);ch.generate();this.chunks.set(this.key(cx,cz),ch)}
return ch;
}
getBlock(wx,wy,wz){
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
const ch=this.getChunk(cx,cz);
return ch?ch.getBlock(wx,wy,wz):B.AIR;
}
setBlock(wx,wy,wz,t){
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
const ch=this.ensureChunk(cx,cz);
ch.setBlock(wx,wy,wz,t);
const lx=wx-cx*CS,lz=wz-cz*CS;
if(lx===0){const n=this.getChunk(cx-1,cz);if(n)n.dirty=true}
if(lx===CS-1){const n=this.getChunk(cx+1,cz);if(n)n.dirty=true}
if(lz===0){const n=this.getChunk(cx,cz-1);if(n)n.dirty=true}
if(lz===CS-1){const n=this.getChunk(cx,cz+1);if(n)n.dirty=true}
}
updateMeshes(playerX,playerZ){
const pcx=Math.floor(playerX/CS),pcz=Math.floor(playerZ/CS);
const R=5;
for(const[key,ch]of this.chunks){
const dx=ch.cx-pcx,dz=ch.cz-pcz;
if(Math.abs(dx)>R+2||Math.abs(dz)>R+2){if(ch.mesh){this.scene.remove(ch.mesh);ch.mesh.geometry.dispose();ch.mesh.material.dispose()}this.chunks.delete(key);continue}
ch.buildMesh(this);
}
for(let dx=-R;dx<=R;dx++){for(let dz=-R;dz<=R;dz++){
const cx=pcx+dx,cz=pcz+dz;
const ch=this.ensureChunk(cx,cz);ch.buildMesh(this);
}}
if(!this.structures.has('built')){this.buildStructures();this.structures.add('built')}
}
buildStructures(){
this.buildCrystalTower(40,40);this.buildStoneCircle(-40,30);
this.buildDragonRuins(30,-40);this.buildSkyIsland(-30,-30);
this.buildAncientLibrary(60,-20);
}
buildCrystalTower(sx,sz){
const h=getTerrainHeight(sx,sz);
for(let y=h+1;y<=h+25;y++){
const r=y<h+15?3:Math.max(1,3-Math.floor((y-h-15)/5));
for(let x=-r;x<=r;x++){for(let z=-r;z<=r;z++){
if(x===0&&z===0&&y<h+25)continue;
if(Math.abs(x)===r&&Math.abs(z)===r&&Math.random()>0.5)continue;
this.setBlock(sx+x,y,sz+z,B.Crystal);
}}
}
for(let i=0;i<6;i++){
const a=i/6*Math.PI*2,r=5;
const bx=sx+Math.round(Math.cos(a)*r),bz=sz+Math.round(Math.sin(a)*r);
const bh=getTerrainHeight(bx,bz);
for(let y=bh+1;y<=h+10;y++)this.setBlock(bx,y,bz,B.DarkStone);
}
for(let x=-1;x<=1;x++)for(let z=-1;z<=1;z++)this.setBlock(sx+x,h+26,sz+z,B.Glowstone);
this.setBlock(sx,h+27,sz,B.Amethyst);
}
buildStoneCircle(sx,sz){
const h=getTerrainHeight(sx,sz);
for(let x=-6;x<=6;x++)for(let z=-6;z<=6;z++){
if(x*x+z*z<=49&&x*x+z*z>=16){const y=h+1;if(x*x+z*z<=30)this.setBlock(sx+x,y,sz+z,B.Stone)}
}
for(let i=0;i<8;i++){
const a=i/8*Math.PI*2,r=5.5;
const bx=sx+Math.round(Math.cos(a)*r),bz=sz+Math.round(Math.sin(a)*r);
const bh=getTerrainHeight(bx,bz);
for(let y=bh+1;y<=bh+6+(i%2)*3;y++)this.setBlock(bx,y,bz,B.Stone);
this.setBlock(bx,bh+7+(i%2)*3,bz,B.Glowstone);
}
this.setBlock(sx,h+1,sz,B.Gold);this.setBlock(sx,h+2,sz,B.Glowstone);
}
buildDragonRuins(sx,sz){
const h=getTerrainHeight(sx,sz);
for(let i=0;i<12;i++){
const a=i/12*Math.PI*2;
const bx=sx+Math.round(Math.cos(a)*(3+i%3)),bz=sz+Math.round(Math.sin(a)*(3+i%3));
const bh=getTerrainHeight(bx,bz);
const rh=2+Math.floor(Math.random()*4);
for(let y=bh+1;y<=bh+rh;y++)this.setBlock(bx,y,bz,B.Bone);
}
for(let x=-2;x<=2;x++)for(let z=-4;z<=4;z++){
if(Math.abs(z)>2&&Math.random()>0.3)continue;
this.setBlock(sx+x,h+1,sz+z,B.DarkStone);
}
for(let i=0;i<3;i++){
const bx=sx-3+i*3,bz=sz;
const bh=getTerrainHeight(bx,bz);
this.setBlock(bx,bh+1,bz,B.Bone);this.setBlock(bx,bh+2,bz,B.Obsidian);
}
this.setBlock(sx,h+4,sz,B.Glowstone);
}
buildSkyIsland(sx,sz){
const baseY=50;
for(let x=-5;x<=5;x++)for(let z=-5;z<=5;z++){
const d=Math.sqrt(x*x+z*z);
if(d>5)continue;
const th=Math.floor(4*(1-d/5));
for(let y=-th;y<=0;y++){
const bt=y===0?B.GRASS:y>-2?B.DIRT:B.Stone;
this.setBlock(sx+x,baseY+y,sz+z,bt);
}
}
for(let i=0;i<3;i++){
const tx=sx+Math.floor(Math.random()*6)-3,tz=sz+Math.floor(Math.random()*6)-3;
const td=Math.sqrt((tx-sx)**2+(tz-sz)**2);
if(td>4)continue;
for(let y=1;y<=4+Math.floor(Math.random()*3);y++)this.setBlock(tx,baseY+y,tz,B.Wood);
for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++){
if(Math.abs(dx)===2&&Math.abs(dz)===2&&Math.random()>0.5)continue;
this.setBlock(tx+dx,baseY+5+Math.floor(Math.random()*2),tz+dz,B.LEAVES);
}
}
this.setBlock(sx,baseY+1,sz,B.Glowstone);
this.setBlock(sx,baseY+2,sz,B.Amethyst);
}
buildAncientLibrary(sx,sz){
const h=getTerrainHeight(sx,sz);
for(let x=-4;x<=4;x++)for(let z=-3;z<=3;z++){
this.setBlock(sx+x,h+1,sz+z,B.DarkStone);
if(Math.abs(x)===4||Math.abs(z)===3){
for(let y=h+2;y<=h+7;y++)this.setBlock(sx+x,y,sz+z,B.Brick);
}
}
for(let x=-3;x<=3;x+=2){
for(let y=h+2;y<=h+5;y++){this.setBlock(sx+x,y,sz-2,B.Wood);this.setBlock(sx+x,y,sz+2,B.Wood)}
}
for(let z=-2;z<=2;z++){
this.setBlock(sx-3,h+6,sz+z,B.Brick);this.setBlock(sx+3,h+6,sz+z,B.Brick);
}
for(let x=-2;x<=2;x++)for(let z=-1;z<=1;z++)this.setBlock(sx+x,h+7,sz+z,B.Brick);
this.setBlock(sx,h+8,sz,B.Glowstone);
for(let i=0;i<4;i++){
const a=i/4*Math.PI*2;
this.setBlock(sx+Math.round(Math.cos(a)*5),h+1,sz+Math.round(Math.sin(a)*5),B.Glowstone);
}
}
}
// ============================================================
// Atlas texture
// ============================================================
let atlas;
function genAtlas(){
const s=16,n=4,c=document.createElement('canvas');c.width=c.height=s*n;
const ctx=c.getContext('2d');
const order=['dirt','grass_side','grass_top','stone','sand','water','wood_side','wood_top','leaves','brick','obsidian','crystal','gold','glowstone','bone','amethyst','darkstone','dirt','dirt','dirt','dirt','dirt','dirt','dirt'];
for(let i=0;i<n*n;i++){
const tn=order[i]||'dirt';
const tc=textures[tn]||textures.dirt;
const col=i%n,row=Math.floor(i/n);
ctx.drawImage(tc,col*s,row*s,s,s);
}
const tex=new THREE.CanvasTexture(c);
tex.magFilter=THREE.NearestFilter;tex.minFilter=THREE.NearestFilter;tex.wrapS=tex.wrapT=THREE.RepeatWrapping;
atlas=tex;
}
// ============================================================
// Player
// ============================================================
class Player{
constructor(camera){
this.camera=camera;
this.pos=new THREE.Vector3(8,40,8);
this.vel=new THREE.Vector3(0,0,0);
this.yaw=0;this.pitch=0;
this.onGround=false;this.flying=false;
this.keys={};
this.height=1.7;this.speed=5;this.flySpeed=10;this.jumpVel=8;
this.selectedSlot=0;
document.addEventListener('keydown',e=>{
this.keys[e.code]=true;
if(e.code==='KeyF'){this.flying=!this.flying;this.vel.y=0}
if(e.code>='Digit1'&&e.code<='Digit9'){this.selectedSlot=parseInt(e.code[5])-1;updateHotbar()}
if(e.code==='Space'&&this.flying)this.vel.y=this.flySpeed;
});
document.addEventListener('keyup',e=>{this.keys[e.code]=false;if(e.code==='Space'&&this.flying)this.vel.y=0});
document.addEventListener('wheel',e=>{this.selectedSlot=(this.selectedSlot+(e.deltaY>0?1:-1)+9)%9;updateHotbar()});
document.addEventListener('mousemove',e=>{
if(document.pointerLockElement!==document.body)return;
this.yaw-=e.movementX*0.002;this.pitch-=e.movementY*0.002;
this.pitch=Math.max(-Math.PI/2+0.01,Math.min(Math.PI/2-0.01,this.pitch));
});
document.addEventListener('mousedown',e=>{
if(document.pointerLockElement!==document.body)return;
if(e.button===0)breakBlock();else if(e.button===2)placeBlock();
});
document.addEventListener('contextmenu',e=>e.preventDefault());
}
update(dt,world){
const sp=this.flying?this.flySpeed:this.speed;
const fwd=new THREE.Vector3(-Math.sin(this.yaw),0,-Math.cos(this.yaw));
const right=new THREE.Vector3(Math.cos(this.yaw),0,-Math.sin(this.yaw));
const move=new THREE.Vector3(0,0,0);
if(this.keys['KeyW'])move.add(fwd);if(this.keys['KeyS'])move.sub(fwd);
if(this.keys['KeyA'])move.sub(right);if(this.keys['KeyD'])move.add(right);
if(move.length()>0)move.normalize().multiplyScalar(sp);
this.vel.x=move.x;this.vel.z=move.z;
if(!this.flying){
this.vel.y-=20*dt;
if(this.keys['Space']&&this.onGround){this.vel.y=this.jumpVel}
}
const isSolid=(bx,by,bz)=>{const t=world.getBlock(bx,by,bz);return t!==B.AIR&&t!==B.WATER};
const halfW=0.3,h=this.height;
// Move X
this.pos.x+=this.vel.x*dt;
if(isSolid(Math.floor(this.pos.x+halfW),Math.floor(this.pos.y-1),Math.floor(this.pos.z))||
isSolid(Math.floor(this.pos.x+halfW),Math.floor(this.pos.y-0.1),Math.floor(this.pos.z))||
isSolid(Math.floor(this.pos.x-halfW),Math.floor(this.pos.y-1),Math.floor(this.pos.z))||
isSolid(Math.floor(this.pos.x-halfW),Math.floor(this.pos.y-0.1),Math.floor(this.pos.z))){
this.pos.x-=this.vel.x*dt;this.vel.x=0}
// Move Z
this.pos.z+=this.vel.z*dt;
if(isSolid(Math.floor(this.pos.x),Math.floor(this.pos.y-1),Math.floor(this.pos.z+halfW))||
isSolid(Math.floor(this.pos.x),Math.floor(this.pos.y-0.1),Math.floor(this.pos.z+halfW))||
isSolid(Math.floor(this.pos.x),Math.floor(this.pos.y-1),Math.floor(this.pos.z-halfW))||
isSolid(Math.floor(this.pos.x),Math.floor(this.pos.y-0.1),Math.floor(this.pos.z-halfW))){
this.pos.z-=this.vel.z*dt;this.vel.z=0}
// Move Y
this.pos.y+=this.vel.y*dt;
const feetY=this.pos.y-h;
if(this.vel.y<=0&&isSolid(Math.floor(this.pos.x),Math.floor(feetY),Math.floor(this.pos.z))){
this.pos.y=Math.floor(feetY)+1+h;this.vel.y=0;this.onGround=true;
}else if(this.vel.y>0&&isSolid(Math.floor(this.pos.x),Math.floor(this.pos.y),Math.floor(this.pos.z))){
this.pos.y=Math.floor(this.pos.y);this.vel.y=0;
}else{
this.onGround=false;
}
this.camera.position.copy(this.pos);
const lookDir=new THREE.Vector3(-Math.sin(this.yaw)*Math.cos(this.pitch),Math.sin(this.pitch),-Math.cos(this.yaw)*Math.cos(this.pitch));
this.camera.lookAt(this.pos.clone().add(lookDir));
}
}
// ============================================================
// Raycasting
// ============================================================
const raycaster=new THREE.Raycaster();
raycaster.far=8;
function raycast(world,camera){
raycaster.setFromCamera({x:0,y:0},camera);
const dir=raycaster.ray.direction;
const origin=raycaster.ray.origin.clone();
const step=0.1;
let prev=null;
for(let t=0;t<8;t+=step){
const p=origin.clone().add(dir.clone().multiplyScalar(t));
const bx=Math.floor(p.x),by=Math.floor(p.y),bz=Math.floor(p.z);
const bt=world.getBlock(bx,by,bz);
if(bt!==B.AIR&&bt!==B.WATER){
return{pos:{x:bx,y:by,z:bz},place:prev,bt};
}
prev={x:bx,y:by,z:bz};
}
return null;
}
function breakBlock(){
if(!currentWorld||!player)return;
const hit=raycast(currentWorld,player.camera);
if(hit){currentWorld.setBlock(hit.pos.x,hit.pos.y,hit.pos.z,B.AIR)}
}
function placeBlock(){
if(!currentWorld||!player)return;
const hit=raycast(currentWorld,player.camera);
if(!hit||!hit.place)return;
const p=hit.place;
const pp=player.pos;
if(Math.abs(p.x+0.5-pp.x)<1&&Math.abs(p.z+0.5-pp.z)<1&&p.y+1>=pp.y-1.7&&p.y<=pp.y+0.1)return;
currentWorld.setBlock(p.x,p.y,p.z,HOTBAR_BLOCKS[player.selectedSlot]);
}
// ============================================================
// Hotbar
// ============================================================
function buildHotbar(){
const hb=document.getElementById('hotbar');hb.innerHTML='';
HOTBAR_BLOCKS.forEach((bt,i)=>{
const slot=document.createElement('div');slot.className='slot'+(i===player?.selectedSlot?' active':'');
const num=document.createElement('div');num.className='num';num.textContent=i+1;slot.appendChild(num);
const c=document.createElement('canvas');c.width=c.height=16;
const ctx=c.getContext('2d');
const tn=getTexName(bt,1).split('.')[1];
ctx.drawImage(textures[tn]||textures.stone,0,0,16,16);
slot.appendChild(c);
const nm=document.createElement('div');nm.className='bname';nm.textContent=BNAME[bt]||'?';slot.appendChild(nm);
hb.appendChild(slot);
});
}
function updateHotbar(){buildHotbar()}
// ============================================================
// Highlight
// ============================================================
let highlightMesh;
function createHighlight(){
const geo=new THREE.BoxGeometry(1.01,1.01,1.01);
const edges=new THREE.EdgesGeometry(geo);
highlightMesh=new THREE.LineSegments(edges,new THREE.LineBasicMaterial({color:0xffffff,transparent:true,opacity:0.5}));
highlightMesh.visible=false;
}
// ============================================================
// Main
// ============================================================
let currentWorld,player;
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.setClearColor(0x87CEEB);
document.body.prepend(renderer.domElement);
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0x87CEEB,60,120);
const camera=new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,200);
const ambientLight=new THREE.AmbientLight(0x6688aa,0.6);scene.add(ambientLight);
const sunLight=new THREE.DirectionalLight(0xfff5e0,1.0);
sunLight.position.set(50,80,30);sunLight.castShadow=true;
sunLight.shadow.mapSize.set(2048,2048);sunLight.shadow.camera.near=0.5;sunLight.shadow.camera.far=200;
sunLight.shadow.camera.left=-60;sunLight.shadow.camera.right=60;sunLight.shadow.camera.top=60;sunLight.shadow.camera.bottom=-60;
scene.add(sunLight);
genTextures();genAtlas();
currentWorld=new World(scene);
player=new Player(camera);
createHighlight();scene.add(highlightMesh);
// Sky dome
const skyGeo=new THREE.SphereGeometry(150,16,16);
const skyMat=new THREE.MeshBasicMaterial({color:0x87CEEB,side:THREE.BackSide});
scene.add(new THREE.Mesh(skyGeo,skyMat));
// Sun visual
const sunGeo=new THREE.SphereGeometry(5,16,16);
const sunMat=new THREE.MeshBasicMaterial({color:0xFFDD44});
const sunMesh=new THREE.Mesh(sunGeo,sunMat);
sunMesh.position.copy(sunLight.position);scene.add(sunMesh);
// Init position
const initH=getTerrainHeight(8,8);
player.pos.set(8,initH+3,8);
currentWorld.updateMeshes(player.pos.x,player.pos.z);
buildHotbar();
let lastTime=performance.now(),frameCount=0,fps=0,fpsTime=0;
const clock=new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05);
const now=performance.now();
frameCount++;fpsTime+=dt;
if(fpsTime>=1){fps=Math.round(frameCount/fpsTime);frameCount=0;fpsTime=0}
player.update(dt,currentWorld);
currentWorld.updateMeshes(player.pos.x,player.pos.z);
const hit=raycast(currentWorld,camera);
if(hit){highlightMesh.visible=true;highlightMesh.position.set(hit.pos.x+0.5,hit.pos.y+0.5,hit.pos.z+0.5)}
else{highlightMesh.visible=false}
sunMesh.position.set(player.pos.x+50,80,player.pos.z+30);
document.getElementById('hud').innerHTML=`FPS: ${fps}<br>XYZ: ${player.pos.x.toFixed(1)}, ${player.pos.y.toFixed(1)}, ${player.pos.z.toFixed(1)}<br>Mode: ${player.flying?'Flying':'Walking'}`;
renderer.render(scene,camera);
}
// Pointer lock
const blocker=document.getElementById('blocker');
document.getElementById('startBtn').addEventListener('click',()=>{document.body.requestPointerLock()});
document.addEventListener('pointerlockchange',()=>{
if(document.pointerLockElement===document.body){blocker.style.display='none'}
else{blocker.style.display='flex'}
});
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
animate();
</script>
</body>
</html>
@@ -0,0 +1,329 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600" width="800" height="600">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a6bc4"/>
<stop offset="40%" stop-color="#4db8ff"/>
<stop offset="100%" stop-color="#87ceeb"/>
</linearGradient>
<linearGradient id="ocean" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#1a8fcc"/>
<stop offset="50%" stop-color="#2196f3"/>
<stop offset="100%" stop-color="#0d47a1"/>
</linearGradient>
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f0d9a0"/>
<stop offset="100%" stop-color="#d4a960"/>
</linearGradient>
<linearGradient id="sunGlow" cx="50%" cy="50%" r="50%" fx="50%" fy="50%" gradientUnits="objectBoundingBox">
<stop offset="0%" stop-color="#fff5c0" stop-opacity="1"/>
<stop offset="50%" stop-color="#ffd54f" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#ff8f00" stop-opacity="0"/>
</linearGradient>
<radialGradient id="sunGlow2" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff5c0" stop-opacity="1"/>
<stop offset="50%" stop-color="#ffd54f" stop-opacity="0.4"/>
<stop offset="100%" stop-color="#ff8f00" stop-opacity="0"/>
</radialGradient>
<linearGradient id="palmTrunk" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#5d4037"/>
<stop offset="50%" stop-color="#795548"/>
<stop offset="100%" stop-color="#4e342e"/>
</linearGradient>
<linearGradient id="bodyGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f5f5f5"/>
<stop offset="60%" stop-color="#e8e8e8"/>
<stop offset="100%" stop-color="#bdbdbd"/>
</linearGradient>
<linearGradient id="wingGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#e0e0e0"/>
<stop offset="100%" stop-color="#9e9e9e"/>
</linearGradient>
<linearGradient id="beakGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#ff8f00"/>
<stop offset="40%" stop-color="#f57c00"/>
<stop offset="100%" stop-color="#e65100"/>
</linearGradient>
<linearGradient id="pouchGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffcc80"/>
<stop offset="100%" stop-color="#ff9800"/>
</linearGradient>
<linearGradient id="wheelMetal" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#e0e0e0"/>
<stop offset="50%" stop-color="#bdbdbd"/>
<stop offset="100%" stop-color="#9e9e9e"/>
</linearGradient>
<linearGradient id="frameGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f44336"/>
<stop offset="100%" stop-color="#c62828"/>
</linearGradient>
<filter id="shadow" x="-5%" y="-5%" width="115%" height="115%">
<feDropShadow dx="2" dy="3" stdDeviation="2" flood-opacity="0.2"/>
</filter>
<filter id="softShadow" x="-10%" y="-10%" width="125%" height="125%">
<feDropShadow dx="1" dy="2" stdDeviation="3" flood-opacity="0.15"/>
</filter>
<clipPath id="sceneClip">
<rect x="0" y="0" width="800" height="600" rx="0"/>
</clipPath>
</defs>
<!-- Background -->
<g clip-path="url(#sceneClip)">
<!-- Sky -->
<rect width="800" height="600" fill="url(#sky)"/>
<!-- Sun -->
<circle cx="650" cy="100" r="60" fill="url(#sunGlow2)" opacity="0.8"/>
<circle cx="650" cy="100" r="35" fill="#fff9c4" opacity="0.9"/>
<circle cx="650" cy="100" r="28" fill="#ffeb3b"/>
<!-- Clouds -->
<g opacity="0.85">
<ellipse cx="150" cy="90" rx="60" ry="22" fill="white" opacity="0.9"/>
<ellipse cx="120" cy="85" rx="40" ry="18" fill="white" opacity="0.95"/>
<ellipse cx="180" cy="88" rx="45" ry="20" fill="white" opacity="0.9"/>
<ellipse cx="155" cy="78" rx="35" ry="15" fill="white"/>
<ellipse cx="450" cy="60" rx="50" ry="18" fill="white" opacity="0.8"/>
<ellipse cx="430" cy="55" rx="35" ry="14" fill="white" opacity="0.85"/>
<ellipse cx="475" cy="58" rx="40" ry="16" fill="white" opacity="0.8"/>
<ellipse cx="320" cy="120" rx="55" ry="20" fill="white" opacity="0.75"/>
<ellipse cx="295" cy="115" rx="38" ry="15" fill="white" opacity="0.8"/>
<ellipse cx="345" cy="118" rx="42" ry="17" fill="white" opacity="0.75"/>
</g>
<!-- Ocean -->
<rect x="0" y="280" width="800" height="120" fill="url(#ocean)"/>
<!-- Ocean waves -->
<g fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="1.5">
<path d="M0,300 Q50,293 100,300 Q150,307 200,300 Q250,293 300,300 Q350,307 400,300 Q450,293 500,300 Q550,307 600,300 Q650,293 700,300 Q750,307 800,300"/>
<path d="M0,315 Q40,308 80,315 Q120,322 160,315 Q200,308 240,315 Q280,322 320,315 Q360,308 400,315 Q440,322 480,315 Q520,308 560,315 Q600,322 640,315 Q680,308 720,315 Q760,322 800,315"/>
<path d="M0,330 Q45,323 90,330 Q135,337 180,330 Q225,323 270,330 Q315,337 360,330 Q405,323 450,330 Q495,337 540,330 Q585,323 630,330 Q675,337 720,330 Q765,323 800,330"/>
<path d="M0,348 Q60,341 120,348 Q180,355 240,348 Q300,341 360,348 Q420,355 480,348 Q540,341 600,348 Q660,355 720,348 Q780,341 800,345"/>
</g>
<!-- Foam line -->
<path d="M0,390 Q30,385 60,388 Q90,391 120,387 Q150,383 180,387 Q210,391 240,386 Q270,381 300,386 Q330,391 360,386 Q390,381 420,387 Q450,393 480,387 Q510,381 540,387 Q570,393 600,387 Q630,381 660,387 Q690,393 720,387 Q750,381 780,386 L800,385 L800,400 L0,400 Z" fill="white" opacity="0.5"/>
<!-- Beach/Sand -->
<path d="M0,400 Q100,385 200,395 Q300,405 400,390 Q500,375 600,388 Q700,401 800,392 L800,600 L0,600 Z" fill="url(#sand)"/>
<!-- Wet sand near water -->
<path d="M0,400 Q100,385 200,395 Q300,405 400,390 Q500,375 600,388 Q700,401 800,392 L800,415 Q700,418 600,412 Q500,406 400,415 Q300,424 200,414 Q100,404 0,412 Z" fill="#c9a96e" opacity="0.6"/>
<!-- Palm tree left -->
<g transform="translate(80,280)">
<path d="M0,120 Q5,80 -5,40 Q-8,20 2,0" stroke="url(#palmTrunk)" stroke-width="10" fill="none" stroke-linecap="round"/>
<path d="M-5,35 Q-15,30 -35,20" stroke="url(#palmTrunk)" stroke-width="5" fill="none"/>
<!-- Leaves -->
<g fill="#2e7d32" opacity="0.9">
<path d="M2,0 Q-30,-15 -55,-8 Q-40,-25 -2,-5 Z"/>
<path d="M2,0 Q25,-20 50,-15 Q35,-30 5,-8 Z"/>
<path d="M2,0 Q-20,-30 -40,-35 Q-25,-45 0,-12 Z"/>
<path d="M2,0 Q20,-35 38,-38 Q30,-48 5,-15 Z"/>
<path d="M2,0 Q-5,-35 -15,-45 Q0,-50 5,-18 Z"/>
<path d="M2,0 Q10,-35 18,-42 Q8,-50 2,-18 Z"/>
</g>
<!-- Coconuts -->
<circle cx="-3" cy="2" r="4" fill="#5d4037"/>
<circle cx="5" cy="4" r="3.5" fill="#4e342e"/>
<circle cx="1" cy="6" r="3" fill="#6d4c41"/>
</g>
<!-- Palm tree right -->
<g transform="translate(700,290) scale(-1,1)">
<path d="M0,110 Q8,70 -2,35 Q-5,15 5,0" stroke="url(#palmTrunk)" stroke-width="9" fill="none" stroke-linecap="round"/>
<g fill="#2e7d32" opacity="0.85">
<path d="M5,0 Q-25,-18 -48,-12 Q-35,-28 3,-8 Z"/>
<path d="M5,0 Q28,-22 48,-18 Q38,-32 8,-10 Z"/>
<path d="M5,0 Q-18,-32 -35,-38 Q-22,-48 3,-14 Z"/>
<path d="M5,0 Q22,-35 36,-40 Q28,-50 7,-16 Z"/>
<path d="M5,0 Q-3,-38 -10,-48 Q3,-52 6,-20 Z"/>
</g>
<circle cx="3" cy="2" r="3.5" fill="#5d4037"/>
<circle cx="8" cy="4" r="3" fill="#4e342e"/>
</g>
<!-- Seagulls -->
<g fill="none" stroke="#555" stroke-width="1.5" stroke-linecap="round" opacity="0.6">
<path d="M520,140 Q525,135 530,138 Q535,135 540,140"/>
<path d="M560,120 Q564,116 568,118 Q572,116 576,120"/>
<path d="M490,160 Q493,156 496,158 Q499,156 502,160"/>
</g>
<!-- ========== PELICAN ON BICYCLE ========== -->
<g transform="translate(280, 310)" filter="url(#softShadow)">
<!-- Bicycle -->
<g>
<!-- Back wheel -->
<circle cx="-50" cy="70" r="38" fill="none" stroke="#333" stroke-width="3"/>
<circle cx="-50" cy="70" r="35" fill="none" stroke="#555" stroke-width="1.5"/>
<circle cx="-50" cy="70" r="3" fill="#333"/>
<!-- Back wheel spokes -->
<g stroke="#777" stroke-width="0.8">
<line x1="-50" y1="32" x2="-50" y2="108"/>
<line x1="-88" y1="70" x2="-12" y2="70"/>
<line x1="-77" y1="43" x2="-23" y2="97"/>
<line x1="-23" y1="43" x2="-77" y2="97"/>
<line x1="-62" y1="35" x2="-38" y2="105"/>
<line x1="-38" y1="35" x2="-62" y2="105"/>
</g>
<!-- Front wheel -->
<circle cx="70" cy="70" r="38" fill="none" stroke="#333" stroke-width="3"/>
<circle cx="70" cy="70" r="35" fill="none" stroke="#555" stroke-width="1.5"/>
<circle cx="70" cy="70" r="3" fill="#333"/>
<!-- Front wheel spokes -->
<g stroke="#777" stroke-width="0.8">
<line x1="70" y1="32" x2="70" y2="108"/>
<line x1="32" y1="70" x2="108" y2="70"/>
<line x1="43" y1="43" x2="97" y2="97"/>
<line x1="97" y1="43" x2="43" y2="97"/>
<line x1="58" y1="35" x2="82" y2="105"/>
<line x1="82" y1="35" x2="58" y2="105"/>
</g>
<!-- Tires (rubber) -->
<circle cx="-50" cy="70" r="38" fill="none" stroke="#222" stroke-width="5" opacity="0.3"/>
<circle cx="70" cy="70" r="38" fill="none" stroke="#222" stroke-width="5" opacity="0.3"/>
<!-- Frame -->
<g stroke="url(#frameGrad)" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round">
<!-- Seat tube -->
<line x1="-10" y1="15" x2="-25" y2="55"/>
<!-- Top tube -->
<line x1="-10" y1="15" x2="40" y2="25"/>
<!-- Down tube -->
<line x1="40" y1="25" x2="-25" y2="55"/>
<!-- Chain stay -->
<line x1="-25" y1="55" x2="-50" y2="70"/>
<!-- Seat stay -->
<line x1="-10" y1="15" x2="-50" y2="70"/>
<!-- Head tube / fork -->
<line x1="40" y1="25" x2="55" y2="45"/>
<line x1="55" y1="45" x2="70" y2="70"/>
<!-- Handlebar stem -->
<line x1="40" y1="25" x2="50" y2="8"/>
<!-- Handlebar -->
<line x1="42" y1="5" x2="58" y2="5"/>
<circle cx="42" cy="5" r="3" fill="#333"/>
<circle cx="58" cy="5" r="3" fill="#333"/>
</g>
<!-- Seat -->
<ellipse cx="-10" cy="12" rx="12" ry="4" fill="#333"/>
<ellipse cx="-10" cy="11" rx="10" ry="3" fill="#555"/>
<!-- Pedals & crank -->
<circle cx="-25" cy="55" r="8" fill="none" stroke="#666" stroke-width="2"/>
<circle cx="-25" cy="55" r="2" fill="#444"/>
<line x1="-25" y1="47" x2="-25" y2="63" stroke="#666" stroke-width="2.5"/>
<rect x="-32" y="45" width="7" height="3" rx="1" fill="#888" transform="rotate(-30,-28.5,46.5)"/>
<rect x="-22" y="61" width="7" height="3" rx="1" fill="#888" transform="rotate(-30,-18.5,62.5)"/>
<!-- Chain -->
<ellipse cx="-37" cy="62" rx="16" ry="10" fill="none" stroke="#888" stroke-width="1" stroke-dasharray="2,2"/>
<!-- Fender -->
<path d="M-85,45 Q-50,20 -15,45" fill="none" stroke="#c62828" stroke-width="2.5" opacity="0.7"/>
</g>
<!-- ========== PELICAN ========== -->
<g transform="translate(-15, -30)">
<!-- Tail feathers -->
<path d="M-35,40 Q-55,30 -60,45 Q-50,38 -40,48 Z" fill="#9e9e9e" opacity="0.8"/>
<path d="M-30,42 Q-52,35 -55,50 Q-45,42 -35,50 Z" fill="#bdbdbd" opacity="0.7"/>
<!-- Body -->
<ellipse cx="0" cy="25" rx="40" ry="22" fill="url(#bodyGrad)" stroke="#ccc" stroke-width="0.5"/>
<!-- Body shading -->
<ellipse cx="5" cy="30" rx="30" ry="15" fill="#e0e0e0" opacity="0.5"/>
<!-- Wing (folded) -->
<path d="M-10,10 Q-35,15 -45,35 Q-30,30 -5,25 Z" fill="url(#wingGrad)" stroke="#bbb" stroke-width="0.5"/>
<!-- Wing feather details -->
<path d="M-8,15 Q-25,18 -35,30" fill="none" stroke="#aaa" stroke-width="0.7" opacity="0.6"/>
<path d="M-5,18 Q-20,22 -28,32" fill="none" stroke="#aaa" stroke-width="0.7" opacity="0.5"/>
<path d="M-3,21 Q-15,25 -22,33" fill="none" stroke="#aaa" stroke-width="0.7" opacity="0.4"/>
<!-- Wing (raised/active) -->
<path d="M5,10 Q25,-15 45,-25 Q35,-10 20,5 Q30,0 40,-18 Q32,-5 18,8 Z" fill="#e8e8e8" stroke="#ccc" stroke-width="0.5" opacity="0.9"/>
<path d="M10,5 Q30,-12 42,-22" fill="none" stroke="#bbb" stroke-width="0.7" opacity="0.5"/>
<path d="M15,2 Q28,-8 38,-18" fill="none" stroke="#bbb" stroke-width="0.7" opacity="0.4"/>
<!-- Legs (cycling posture) -->
<!-- Right leg (on pedal) -->
<path d="M10,40 Q15,50 5,62 Q0,55 8,45" fill="#ff8f00" stroke="#e65100" stroke-width="1"/>
<!-- Left leg (extended) -->
<path d="M-5,42 Q-10,52 -15,58 Q-12,55 -2,48" fill="#ff8f00" stroke="#e65100" stroke-width="1"/>
<!-- Feet -->
<path d="M5,62 Q2,66 -3,65 Q0,63 5,62" fill="#ff8f00" stroke="#e65100" stroke-width="0.5"/>
<path d="M-15,58 Q-18,62 -23,61 Q-20,59 -15,58" fill="#ff8f00" stroke="#e65100" stroke-width="0.5"/>
<!-- Neck -->
<path d="M25,15 Q35,-5 40,-30 Q38,-25 30,0 Q28,10 25,15" fill="url(#bodyGrad)" stroke="#ddd" stroke-width="0.5"/>
<!-- Head -->
<ellipse cx="42" cy="-35" rx="12" ry="10" fill="#f5f5f5" stroke="#ddd" stroke-width="0.5"/>
<!-- Head shading -->
<ellipse cx="44" cy="-33" rx="8" ry="6" fill="#eee" opacity="0.5"/>
<!-- Eye -->
<circle cx="48" cy="-37" r="3.5" fill="white" stroke="#333" stroke-width="0.5"/>
<circle cx="49" cy="-37" r="2" fill="#1a237e"/>
<circle cx="49.5" cy="-37.5" r="0.7" fill="white"/>
<!-- Beak (upper) -->
<path d="M50,-36 Q70,-40 82,-35 Q78,-33 65,-32 Q55,-32 50,-34 Z" fill="url(#beakGrad)" stroke="#e65100" stroke-width="0.5"/>
<!-- Beak tip hook -->
<path d="M82,-35 Q84,-34 82,-33" fill="#e65100" stroke="#bf360c" stroke-width="0.5"/>
<!-- Beak (lower) -->
<path d="M50,-33 Q65,-30 78,-33 Q75,-35 65,-34 Q55,-34 50,-33 Z" fill="#ff9800" stroke="#e65100" stroke-width="0.3"/>
<!-- Pouch -->
<path d="M50,-32 Q60,-22 70,-28 Q65,-20 55,-25 Q52,-28 50,-32 Z" fill="url(#pouchGrad)" stroke="#e65100" stroke-width="0.5" opacity="0.85"/>
<!-- Pouch texture -->
<path d="M55,-28 Q60,-24 65,-27" fill="none" stroke="#e65100" stroke-width="0.3" opacity="0.5"/>
<path d="M53,-30 Q58,-26 63,-29" fill="none" stroke="#e65100" stroke-width="0.3" opacity="0.4"/>
<!-- Crown tuft -->
<path d="M38,-42 Q40,-50 42,-45 Q43,-52 45,-46 Q46,-50 44,-42" fill="#9e9e9e" stroke="#bbb" stroke-width="0.3"/>
<!-- Throat patch -->
<ellipse cx="46" cy="-28" rx="5" ry="3" fill="#fff3e0" opacity="0.5"/>
</g>
</g>
<!-- Beach details -->
<!-- Shells -->
<g transform="translate(180,440)" opacity="0.6">
<path d="M0,0 Q3,-5 6,0 Q3,2 0,0" fill="#f5f5dc" stroke="#d4a960" stroke-width="0.5"/>
</g>
<g transform="translate(520,430)" opacity="0.5">
<path d="M0,0 Q2,-4 4,0 Q2,1.5 0,0" fill="#ffe0b2" stroke="#d4a960" stroke-width="0.5"/>
</g>
<g transform="translate(350,455)" opacity="0.4">
<circle cx="0" cy="0" r="2" fill="#f5f5dc"/>
<circle cx="5" cy="2" r="1.5" fill="#ffe0b2"/>
</g>
<!-- Starfish -->
<g transform="translate(600,445) scale(0.5)" opacity="0.5">
<polygon points="0,-12 3,-4 12,-4 5,2 7,10 0,5 -7,10 -5,2 -12,-4 -3,-4" fill="#e57373" stroke="#c62828" stroke-width="0.5"/>
</g>
<!-- Footprints -->
<g fill="#c9a96e" opacity="0.3">
<ellipse cx="420" cy="450" rx="3" ry="5" transform="rotate(-10,420,450)"/>
<ellipse cx="435" cy="455" rx="3" ry="5" transform="rotate(5,435,455)"/>
<ellipse cx="450" cy="448" rx="3" ry="5" transform="rotate(-5,450,448)"/>
</g>
<!-- Shadow under pelican/bicycle -->
<ellipse cx="300" cy="470" rx="80" ry="8" fill="rgba(0,0,0,0.1)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

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