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:
1 parent
acb27270f0
commit
1a6b884f75
4 files changed
+1762
No files matched your search
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"label": "MiMo V2.5",
|
||||
"vendor": "Xiaomi",
|
||||
"harness": "MiMo Code",
|
||||
"effort": "High",
|
||||
"artifactDir": "mimo-v2-5/micode-high",
|
||||
"order": 97,
|
||||
"runs": {
|
||||
"mythos-craft": {
|
||||
"note": {
|
||||
"zh": "在 MiMo Code (v0.1.4) 中以 MiMo V2.5(High 思考强度)一次生成;原始产出修复了多个严重 bug(Perlin noise permutation 数组初始化错误导致 NaN 索引、noise 函数作用域问题、noiseFill 变量命名冲突、函数名误用点号、this 缺少点号、IIFE 未闭合、Three.js RepeatSetting 常量错误、WASD 移动按键错误、沙子/草地判断逻辑反转、物理引擎碰撞检测逻辑错误导致着地弹跳),并重写了整个噪声模块和物理引擎,将 Three.js 内联以支持本地 file:// 协议运行,生成逻辑未改。基于 Three.js 构建了完整的 3D 体素世界,包含柏林噪声生成的地形、WASD + 鼠标第一人称操控、8 种方块类型(泥土、草、石头、沙、木头、砖、水晶、黄金)、左键破坏右键放置、飞行模式、以及五处神话幻想建筑(水晶塔、石阵、龙骨遗迹、天空岛、远古图书馆)。",
|
||||
"en": "Generated in one shot by MiMo V2.5 (High effort) inside MiMo Code (v0.1.4); original output had multiple critical bugs fixed (Perlin noise permutation array initialization error causing NaN indexing, noise function scope issue, noiseFill variable naming collision, dot after function keyword, missing dot after this, unclosed IIFE, Three.js RepeatSetting constant error, WASD movement key error, sand/grass logic inversion, physics engine collision detection error causing ground bouncing); noise module and physics engine rewritten entirely, Three.js inlined for local file:// protocol compatibility; generation logic untouched. Built a complete 3D voxel world with Three.js, featuring Perlin noise terrain, WASD + mouse first-person controls, 8 block types (dirt, grass, stone, sand, wood, brick, crystal, gold), left-click break / right-click place, fly mode, and five mythic-fantasy structures (Crystal Tower, Stone Circle, Dragon Ruins, Sky Island, Ancient Library)."
|
||||
},
|
||||
"contributor": "BaiLin-William"
|
||||
},
|
||||
"pelican-cycling": {
|
||||
"note": {
|
||||
"zh": "在 MiMo Code (v0.1.4) 中以 MiMo V2.5(High 思考强度)一次生成;未经修改的原始产出。包含渐变天空、太阳光晕、多层云朵、海浪纹理、浪花泡沫线、湿润沙滩、棕榈树(含椰子)、海鸥、鹈鹕(解剖细节:喉囊、喙钩、羽冠、翅膀羽毛纹理、蹼足)、红色自行车(辐条、链条、踏板、车座、挡泥板)、海星贝壳脚印等装饰元素。",
|
||||
"en": "Generated in one shot by MiMo V2.5 (High effort) inside MiMo Code (v0.1.4); unmodified original output. Includes gradient sky, sun glow, layered clouds, ocean wave textures, foam line, wet sand, palm trees with coconuts, seagulls, a detailed pelican (anatomical: gular pouch, beak hook, crown tuft, wing feather texture, webbed feet), red bicycle (spokes, chain, pedals, seat, fender), and beach decorations (starfish, shells, footprints)."
|
||||
},
|
||||
"contributor": "BaiLin-William"
|
||||
},
|
||||
"skeleton-watch": {
|
||||
"note": {
|
||||
"zh": "在 MiMo Code (v0.1.4) 中以 MiMo V2.5(High 思考强度)一次生成;将 Three.js 内联以支持本地 file:// 协议运行。原始产出在机芯型号中写出 MiMo,会在 Arena 盲评中泄露身份;本页仅将该型号改为中性 SK-01,其余未修改。基于 Three.js 构建了完整的 3D 镂空机械腕表,包含发条盒(含可见游丝发条)、齿轮系(5 组齿轮)、擒纵机构(擒纵轮+擒纵叉+红宝石瓦)、摆轮(带宝玑游丝、调速螺丝、冲击销)、夹板(日内瓦纹主夹板、摆轮夹板、发条盒夹板)、35 颗红宝石轴承、时分秒针(夜光涂层)、12 个小时刻度(3/6/9/12 为金色条形+夜光点)、60 格分钟刻度、蓝宝石水晶表镜与底盖、玫瑰金表壳表圈、表冠(带防滑纹路)、表耳。交互功能:拖拽旋转、滚轮缩放、E 键切换爆炸视图、I 键显示机芯信息面板、空格暂停动画、悬停零件标签高亮。灯光:ACES 色调映射、三点布光(主光/填充光/轮廓光)+ 顶部补光。",
|
||||
"en": "Generated in one shot by MiMo V2.5 (High effort) inside MiMo Code (v0.1.4); Three.js inlined for local file:// protocol compatibility. The original output included MiMo in the calibre model, which would reveal identity in blind Arena mode; this page only changes that visible model to neutral SK-01, otherwise unmodified. Built a complete 3D skeleton mechanical watch with Three.js, featuring mainspring barrel (visible coiled spring), gear train (5 wheels), escapement (escape wheel + pallet fork with ruby pallets), balance wheel (Breguet hairspring, timing screws, impulse pin), bridges (Côtes de Genève main bridge, balance cock, barrel bridge), 35 ruby jewel bearings, luminous hour/minute/second hands, 12 hour markers (gold bar at 3/6/9/12 with lume dot), 60-minute track, sapphire crystal front and caseback, rose gold case and bezel, knurled crown, and lugs. Interactions: drag rotate, scroll zoom, E for exploded view, I for movement info panel, Space to pause, hover labels. Lighting: ACES tone mapping, three-point studio lighting."
|
||||
},
|
||||
"contributor": "BaiLin-William"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user