Add MiMo v2.5 Pro UltraSpeed runs (mythos-craft, pelican-cycling)
Registers MiMo v2.5 Pro UltraSpeed (Xiaomi, Web · Default) with two one-shot variants each for mythos-craft and pelican-cycling. Verified: SVGs render, both Three.js voxel games run, schema validates, README tables synced (zh+en).
This commit is contained in:
1 parent
b07823f4be
commit
13da28bd8e
7 files changed
+2275
No files matched your search
@@ -0,0 +1,919 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mythicraft — 神话世界</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{overflow:hidden;background:#000;font-family:'Segoe UI',sans-serif;cursor:none}
|
||||
canvas{display:block;width:100vw;height:100vh}
|
||||
#overlay{position:fixed;inset:0;background:rgba(5,5,15,.92);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#ccc;z-index:100;cursor:pointer;transition:opacity .4s}
|
||||
#overlay.hidden{opacity:0;pointer-events:none}
|
||||
#overlay h1{font-size:clamp(36px,6vw,64px);color:#e8c547;text-shadow:0 0 40px rgba(232,197,71,.4),0 0 80px rgba(232,197,71,.15);letter-spacing:.08em;font-weight:300;margin-bottom:8px}
|
||||
#overlay .sub{font-size:15px;color:#888;margin-bottom:24px;letter-spacing:.15em}
|
||||
#overlay .keys{font-size:13px;color:#777;line-height:2}
|
||||
#overlay .keys b{color:#bbb}
|
||||
#overlay .start{margin-top:32px;font-size:20px;color:#e8c547;animation:pulse 2.5s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
||||
#crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:10;display:none}
|
||||
#crosshair::before,#crosshair::after{content:'';position:absolute;background:rgba(255,255,255,.85)}
|
||||
#crosshair::before{width:18px;height:2px;top:-1px;left:-9px}
|
||||
#crosshair::after{width:2px;height:18px;top:-9px;left:-1px}
|
||||
#hotbar{position:fixed;bottom:8px;left:50%;transform:translateX(-50%);display:none;gap:3px;z-index:10}
|
||||
.slot{width:48px;height:48px;border:2px solid rgba(100,100,100,.5);background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;position:relative;image-rendering:pixelated}
|
||||
.slot.active{border-color:#e8c547;background:rgba(232,197,71,.15);box-shadow:0 0 12px rgba(232,197,71,.2)}
|
||||
.slot canvas{width:36px;height:36px;image-rendering:pixelated}
|
||||
.slot .num{position:absolute;top:2px;left:4px;font-size:10px;color:rgba(255,255,255,.4)}
|
||||
#debug{position:fixed;top:8px;left:8px;color:rgba(255,255,255,.7);font-size:11px;font-family:'Consolas',monospace;z-index:10;display:none;text-shadow:0 1px 3px rgba(0,0,0,.8);line-height:1.6;pointer-events:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="overlay">
|
||||
<h1>MYTHICRAFT</h1>
|
||||
<div class="sub">一个充满神话幻想的体素世界</div>
|
||||
<div class="keys">
|
||||
<b>W A S D</b> 移动 <b>空格</b> 跳跃 <b>Shift</b> 下潜<br>
|
||||
<b>鼠标移动</b> 环视 <b>左键</b> 破坏方块 <b>右键</b> 放置方块<br>
|
||||
<b>1-9 / 滚轮</b> 选择方块 <b>F</b> 切换飞行
|
||||
</div>
|
||||
<div class="start">— 点击任意处开始探险 —</div>
|
||||
</div>
|
||||
<div id="crosshair"></div>
|
||||
<div id="hotbar"></div>
|
||||
<div id="debug"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* ======================================================
|
||||
CONSTANTS
|
||||
====================================================== */
|
||||
const CS = 16, CH = 80, RD = 6, WL = 28;
|
||||
const B = {AIR:0,GRASS:1,DIRT:2,STONE:3,SAND:4,WATER:5,WOOD:6,LEAVES:7,BEDROCK:8,GLOW:9,CRYSTAL:10,PLANKS:11,COBBLE:12,BRICK:13,OBSIDIAN:14,SNOW:15,MARBLE:16,MAGIC:17,DSTONE:18,GOLD:19};
|
||||
const BF = [];
|
||||
BF[B.GRASS]=[0,1,2]; BF[B.DIRT]=[2,2,2]; BF[B.STONE]=[3,3,3]; BF[B.SAND]=[4,4,4];
|
||||
BF[B.WATER]=[5,5,5]; BF[B.WOOD]=[6,7,6]; BF[B.LEAVES]=[8,8,8]; BF[B.BEDROCK]=[9,9,9];
|
||||
BF[B.GLOW]=[10,10,10]; BF[B.CRYSTAL]=[11,11,11]; BF[B.PLANKS]=[12,12,12];
|
||||
BF[B.COBBLE]=[13,13,13]; BF[B.BRICK]=[14,14,14]; BF[B.OBSIDIAN]=[15,15,15];
|
||||
BF[B.SNOW]=[16,16,16]; BF[B.MARBLE]=[17,17,17]; BF[B.MAGIC]=[18,18,18];
|
||||
BF[B.DSTONE]=[19,19,19]; BF[B.GOLD]=[20,20,20];
|
||||
const TRANSP = new Set([B.AIR,B.WATER,B.LEAVES,B.CRYSTAL,B.GLOW]);
|
||||
const SOLID = new Set([B.GRASS,B.DIRT,B.STONE,B.SAND,B.WOOD,B.BEDROCK,B.PLANKS,B.COBBLE,B.BRICK,B.OBSIDIAN,B.SNOW,B.MARBLE,B.MAGIC,B.DSTONE,B.GOLD]);
|
||||
const HOTBAR_ITEMS = [B.GRASS,B.DIRT,B.STONE,B.COBBLE,B.BRICK,B.PLANKS,B.OBSIDIAN,B.GLOW,B.CRYSTAL];
|
||||
const BN = {};Object.entries(B).forEach(([k,v])=>BN[v]=k);
|
||||
const AC=16,AR=2,TS=16;
|
||||
const FDIR=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
|
||||
const FVERT=[
|
||||
[[1,0,1],[1,0,0],[1,1,0],[1,1,1]],
|
||||
[[0,0,0],[0,0,1],[0,1,1],[0,1,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]],
|
||||
[[1,0,1],[0,0,1],[0,1,1],[0,1,1]],
|
||||
[[0,0,0],[1,0,0],[1,1,0],[0,1,0]]
|
||||
];
|
||||
const FUVS=[[0,0],[1,0],[1,1],[0,1]];
|
||||
|
||||
/* ======================================================
|
||||
SIMPLEX NOISE
|
||||
====================================================== */
|
||||
class SimplexNoise{
|
||||
constructor(seed=42){
|
||||
this.g=[[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]];
|
||||
const p=new Uint8Array(256);for(let i=0;i<256;i++)p[i]=i;
|
||||
let s=seed;for(let i=255;i>0;i--){s=(s*1103515245+12345)&0x7fffffff;const j=s%(i+1);[p[i],p[j]]=[p[j],p[i]];}
|
||||
this.perm=new Uint8Array(512);this.pm12=new Uint8Array(512);
|
||||
for(let i=0;i<512;i++){this.perm[i]=p[i&255];this.pm12[i]=this.perm[i]%12;}
|
||||
}
|
||||
n2(x,y){
|
||||
const F=.5*(Math.sqrt(3)-1),G=(3-Math.sqrt(3))/6,s=(x+y)*F,i=Math.floor(x+s),j=Math.floor(y+s),t=(i+j)*G;
|
||||
const x0=x-(i-t),y0=y-(j-t),i1=x0>y0?1:0,j1=x0>y0?0:1;
|
||||
const x1=x0-i1+G,y1=y0-j1+G,x2=x0-1+2*G,y2=y0-1+2*G;
|
||||
const ii=i&255,jj=j&255;
|
||||
const d=(g,x,y)=>g[0]*x+g[1]*y;
|
||||
const g0=this.pm12[ii+this.perm[jj]],g1=this.pm12[ii+i1+this.perm[jj+j1]],g2=this.pm12[ii+1+this.perm[jj+1]];
|
||||
let n0,n1,n2,t0=.5-x0*x0-y0*y0;t0=t0<0?0:(t0*=t0,t0*t0*d(this.g[g0],x0,y0));
|
||||
let t1=.5-x1*x1-y1*y1;t1=t1<0?0:(t1*=t1,t1*t1*d(this.g[g1],x1,y1));
|
||||
let t2=.5-x2*x2-y2*y2;t2=t2<0?0:(t2*=t2,t2*t2*d(this.g[g2],x2,y2));
|
||||
return 70*(t0+t1+t2);
|
||||
}
|
||||
n3(x,y,z){
|
||||
const F=1/3,G=1/6,s=(x+y+z)*F,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),t=(i+j+k)*G;
|
||||
const x0=x-(i-t),y0=y-(j-t),z0=z-(k-t);
|
||||
let i1,j1,k1,i2,j2,k2;
|
||||
if(x0>=y0){if(y0>=z0){i1=1;j1=0;k1=0;i2=1;j2=1;k2=0}else if(x0>=z0){i1=1;j1=0;k1=0;i2=1;j2=0;k2=1}else{i1=0;j1=0;k1=1;i2=1;j2=0;k2=1}}
|
||||
else{if(y0<z0){i1=0;j1=0;k1=1;i2=0;j2=1;k2=1}else if(x0<z0){i1=0;j1=1;k1=0;i2=0;j2=1;k2=1}else{i1=0;j1=1;k1=0;i2=1;j2=1;k2=0}}
|
||||
const x1=x0-i1+G,y1=y0-j1+G,z1=z0-k1+G,x2=x0-i2+2*G,y2=y0-j2+2*G,z2=z0-k2+2*G,x3=x0-1+3*G,y3=y0-1+3*G,z3=z0-1+3*G;
|
||||
const ii=i&255,jj=j&255,kk=k&255;
|
||||
const d=(g,a,b,c)=>g[0]*a+g[1]*b+g[2]*c;
|
||||
const g0=this.pm12[ii+this.perm[jj+this.perm[kk]]],g1=this.pm12[ii+i1+this.perm[jj+j1+this.perm[kk+k1]]];
|
||||
const g2=this.pm12[ii+i2+this.perm[jj+j2+this.perm[kk+k2]]],g3=this.pm12[ii+1+this.perm[jj+1+this.perm[kk+1]]];
|
||||
let r=0,t0=.6-x0*x0-y0*y0-z0*z0;if(t0>0){t0*=t0;r+=t0*t0*d(this.g[g0],x0,y0,z0);}
|
||||
let t1=.6-x1*x1-y1*y1-z1*z1;if(t1>0){t1*=t1;r+=t1*t1*d(this.g[g1],x1,y1,z1);}
|
||||
let t2=.6-x2*x2-y2*y2-z2*z2;if(t2>0){t2*=t2;r+=t2*t2*d(this.g[g2],x2,y2,z2);}
|
||||
let t3=.6-x3*x3-y3*y3-z3*z3;if(t3>0){t3*=t3;r+=t3*t3*d(this.g[g3],x3,y3,z3);}
|
||||
return 32*r;
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
TEXTURE ATLAS
|
||||
====================================================== */
|
||||
function createAtlas(){
|
||||
const c=document.createElement('canvas');c.width=AC*TS;c.height=AR*TS;
|
||||
const x=c.getContext('2d');
|
||||
let rng=12345;const R=()=>{rng=(rng*1103515245+12345)&0x7fffffff;return(rng/0x7fffffff);};
|
||||
const h=(a,b)=>(Math.sin(a*12.9898+b*78.233)*43758.5453)%1;
|
||||
function dt(i,fn){
|
||||
const col=i%AC,row=i/AC|0,img=x.createImageData(TS,TS);
|
||||
for(let py=0;py<TS;py++)for(let px=0;px<TS;px++){
|
||||
const[r,g,b]=fn(px,py);const j=(py*TS+px)*4;
|
||||
img.data[j]=Math.max(0,Math.min(255,r|0));
|
||||
img.data[j+1]=Math.max(0,Math.min(255,g|0));
|
||||
img.data[j+2]=Math.max(0,Math.min(255,b|0));
|
||||
img.data[j+3]=255;
|
||||
}
|
||||
x.putImageData(img,col*TS,row*TS);
|
||||
}
|
||||
const n=(a,b,s=1)=>{let v=Math.sin(a*12.9898*s+b*78.233*s+43758.5453);return v-Math.floor(v);};
|
||||
dt(0,(x,y)=>{const v=n(x,y)*35;return[45+v,135+v,28+v*.4]});//grass_top
|
||||
dt(1,(x,y)=>y<3?[48+n(x,y,1)*25,135+n(x,y,1)*25,28+n(x,y,1)*15]:[118+n(x,y,1)*22,83+n(x,y,1)*22,48+n(x,y,1)*22]);//grass_side
|
||||
dt(2,(x,y)=>{const v=n(x,y,2)*28;return[118+v,83+v,48+v]});//dirt
|
||||
dt(3,(x,y)=>{const v=n(x,y,3)*32;return[108+v,108+v,108+v]});//stone
|
||||
dt(4,(x,y)=>{const v=n(x,y,4)*22;return[208+v,188+v,128+v]});//sand
|
||||
dt(5,(x,y)=>{const v=n(x,y,5)*18;return[28+v,75+v,195+v]});//water
|
||||
dt(6,(x,y)=>{const d=Math.sqrt((x-7.5)**2+(y-7.5)**2),r=Math.sin(d*2)*18;return[138+r,98+r,48+r]});//wood_top
|
||||
dt(7,(x,y)=>{const v=n(x,y,7)*22,s=Math.sin(y*.9)*12;return[98+v+s,68+v+s,33+v]});//wood_side
|
||||
dt(8,(x,y)=>{const v=n(x,y,8)*45;return[18+v,95+v,12+v*.5]});//leaves
|
||||
dt(9,(x,y)=>{const v=n(x,y,9)*22;return[28+v,28+v,28+v]});//bedrock
|
||||
dt(10,(x,y)=>{const v=n(x,y,10)*32,g=Math.sin(x*.6+y*.4)*22;return[228+g,208+v,118+v]});//glow
|
||||
dt(11,(x,y)=>{const v=n(x,y,11)*35;return[78+v,148+v,228+v*.5]});//crystal
|
||||
dt(12,(x,y)=>{const v=n(x,y,12)*18,p=(y%4===0)?-35:0;return[168+v+p,128+v+p,68+v+p]});//planks
|
||||
dt(13,(x,y)=>{const v=n(x,y,13)*42;return[88+v,88+v,88+v]});//cobble
|
||||
dt(14,(x,y)=>{const v=n(x,y,14)*18,m=((y%4===0)||(x%8===((y<4)?0:4)))?-45:0;return[158+v+m,68+v+m,48+v+m]});//brick
|
||||
dt(15,(x,y)=>{const v=n(x,y,15)*18;return[13+v,8+v,28+v]});//obsidian
|
||||
dt(16,(x,y)=>{const v=n(x,y,16)*18;return[228+v,233+v,238+v]});//snow
|
||||
dt(17,(x,y)=>{const v=n(x,y,17)*22,ve=Math.sin(x*.35+y*.55)*18;return[208+v+ve,208+v+ve,213+v+ve]});//marble
|
||||
dt(18,(x,y)=>{const v=n(x,y,18)*22,r=Math.sin(x*1.3)*Math.cos(y*1.3)*32;return[38+v+r,18+v,78+v+r]});//magic
|
||||
dt(19,(x,y)=>{const v=n(x,y,19)*28;return[48+v,43+v,53+v]});//darkstone
|
||||
dt(20,(x,y)=>{const v=n(x,y,20)*22;return[208+v,178+v,48+v]});//gold
|
||||
const tex=new THREE.CanvasTexture(c);tex.magFilter=THREE.NearestFilter;tex.minFilter=THREE.NearestFilter;
|
||||
return{canvas:c,texture:tex};
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
CHUNK
|
||||
====================================================== */
|
||||
class Chunk{
|
||||
constructor(cx,cz){this.cx=cx;this.cz=cz;this.blocks=new Uint8Array(CS*CH*CS);this.mesh=null;this.waterMesh=null;this.dirty=true;this.gen=false;}
|
||||
idx(x,y,z){return y*CS*CS+z*CS+x;}
|
||||
get(x,y,z){if(x<0||x>=CS||y<0||y>=CH||z<0||z>=CS)return 0;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){this.blocks[this.idx(x,y,z)]=t;this.dirty=true;}}
|
||||
buildMesh(wgetBlock,mat,wmat,scene){
|
||||
if(!this.dirty)return;this.dirty=false;
|
||||
if(this.mesh){this.mesh.geometry.dispose();scene.remove(this.mesh);}
|
||||
if(this.waterMesh){this.waterMesh.geometry.dispose();scene.remove(this.waterMesh);}
|
||||
const P=[],N=[],U=[],I=[],wP=[],wN=[],wU=[],wI=[];
|
||||
let vc=0,wvc=0;
|
||||
const ox=this.cx*CS,oz=this.cz*CS;
|
||||
for(let y=0;y<CH;y++)for(let z=0;z<CS;z++)for(let x=0;x<CS;x++){
|
||||
const b=this.blocks[this.idx(x,y,z)];if(b===0)continue;
|
||||
const faces=BF[b];if(!faces)continue;
|
||||
const isW=b===B.WATER,wx=ox+x,wz=oz+z;
|
||||
const tArr=[faces[0],faces[1],faces[1],faces[1],faces[1],faces[1]];
|
||||
for(let f=0;f<6;f++){
|
||||
const[dx,dy,dz]=FDIR[f],nx=x+dx,ny=y+dy,nz=z+dz;
|
||||
let nb;
|
||||
if(nx<0||nx>=CS||nz<0||nz>=CS)nb=wgetBlock(wx+dx,y+dy,wz+dz);
|
||||
else if(ny<0||ny>=CH)nb=ny<0?B.BEDROCK:B.AIR;
|
||||
else nb=this.blocks[this.idx(nx,ny,nz)];
|
||||
if(isW){if(nb===B.WATER||(!TRANSP.has(nb)&&nb!==0))continue;}
|
||||
else{if(!TRANSP.has(nb)||nb===b)continue;}
|
||||
const ti=tArr[f],col=ti%AC,row=ti/AC|0;
|
||||
const u0=col/AC,u1=(col+1)/AC,v0=1-(row+1)/AR,v1=1-row/AR;
|
||||
const tp=isW?wP:P,tn=isW?wN:N,tu=isW?wU:U,ti2=isW?wI:I;
|
||||
let vc2=isW?wvc:vc;
|
||||
const fv=FVERT[f],fu=FUVS;
|
||||
for(let v=0;v<4;v++){
|
||||
tp.push(ox+x+fv[v][0],y+fv[v][1],oz+z+fv[v][2]);
|
||||
tn.push(FDIR[f][0],FDIR[f][1],FDIR[f][2]);
|
||||
tu.push(u0+fu[v][0]*(u1-u0),v0+fu[v][1]*(v1-v0));
|
||||
}
|
||||
ti2.push(vc2,vc2+1,vc2+2,vc2,vc2+2,vc2+3);
|
||||
if(isW)wvc+=4;else vc+=4;
|
||||
}
|
||||
}
|
||||
if(P.length){
|
||||
const g=new THREE.BufferGeometry();
|
||||
g.setAttribute('position',new THREE.Float32BufferAttribute(P,3));
|
||||
g.setAttribute('normal',new THREE.Float32BufferAttribute(N,3));
|
||||
g.setAttribute('uv',new THREE.Float32BufferAttribute(U,2));
|
||||
g.setIndex(I);
|
||||
this.mesh=new THREE.Mesh(g,mat);scene.add(this.mesh);
|
||||
}
|
||||
if(wP.length){
|
||||
const g=new THREE.BufferGeometry();
|
||||
g.setAttribute('position',new THREE.Float32BufferAttribute(wP,3));
|
||||
g.setAttribute('normal',new THREE.Float32BufferAttribute(wN,3));
|
||||
g.setAttribute('uv',new THREE.Float32BufferAttribute(wU,2));
|
||||
g.setIndex(wI);
|
||||
this.waterMesh=new THREE.Mesh(g,wmat);scene.add(this.waterMesh);
|
||||
}
|
||||
}
|
||||
dispose(scene){
|
||||
if(this.mesh){this.mesh.geometry.dispose();scene.remove(this.mesh);}
|
||||
if(this.waterMesh){this.waterMesh.geometry.dispose();scene.remove(this.waterMesh);}
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
WORLD
|
||||
====================================================== */
|
||||
class World{
|
||||
constructor(seed=42){this.chunks=new Map();this.noise=new SimplexNoise(seed);this.noise2=new SimplexNoise(seed+1);this.noise3=new SimplexNoise(seed+2);}
|
||||
key(cx,cz){return cx+','+cz;}
|
||||
getChunk(cx,cz){return this.chunks.get(this.key(cx,cz));}
|
||||
getBlock(wx,wy,wz){
|
||||
if(wy<0)return B.BEDROCK;if(wy>=CH)return B.AIR;
|
||||
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
|
||||
const ch=this.chunks.get(this.key(cx,cz));
|
||||
if(!ch||!ch.gen)return B.AIR;
|
||||
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
|
||||
return ch.blocks[ch.idx(lx,wy,lz)];
|
||||
}
|
||||
setBlock(wx,wy,wz,type){
|
||||
if(wy<0||wy>=CH)return;
|
||||
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
|
||||
const ch=this.chunks.get(this.key(cx,cz));if(!ch)return;
|
||||
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
|
||||
ch.blocks[ch.idx(lx,wy,lz)]=type;ch.dirty=true;
|
||||
if(lx===0)this._dirty(cx-1,cz);
|
||||
if(lx===CS-1)this._dirty(cx+1,cz);
|
||||
if(lz===0)this._dirty(cx,cz-1);
|
||||
if(lz===CS-1)this._dirty(cx,cz+1);
|
||||
}
|
||||
_dirty(cx,cz){const ch=this.chunks.get(this.key(cx,cz));if(ch)ch.dirty=true;}
|
||||
genChunk(cx,cz){
|
||||
const ch=new Chunk(cx,cz);const n=this.noise,n2=this.noise2,n3=this.noise3;
|
||||
const ox=cx*CS,oz=cz*CS;
|
||||
for(let lz=0;lz<CS;lz++)for(let lx=0;lx<CS;lx++){
|
||||
const wx=ox+lx,wz=oz+lz;
|
||||
const h1=n.n2(wx*.008,wz*.008)*25,nh2=n2.n2(wx*.025,wz*.025)*12;
|
||||
const mt=Math.max(0,n.n2(wx*.004,wz*.004))*20;
|
||||
const dt=n3.n2(wx*.05,wz*.05)*3;
|
||||
const biome=n2.n2(wx*.005,wz*.005);
|
||||
let height=Math.round(32+h1+nh2+mt+dt);
|
||||
height=Math.max(1,Math.min(CH-2,height));
|
||||
for(let y=0;y<CH;y++){
|
||||
let b=B.AIR;
|
||||
if(y===0)b=B.BEDROCK;
|
||||
else if(y<height-4)b=B.STONE;
|
||||
else if(y<height){
|
||||
b=biome>.35?B.DIRT:B.SAND;
|
||||
if(y<WL&&y>=height-1)b=biome>.35?B.DIRT:B.SAND;
|
||||
}
|
||||
else if(y===height){
|
||||
if(biome>.35){
|
||||
if(height>48)b=B.SNOW;
|
||||
else if(height>WL+2)b=B.STONE;
|
||||
else b=B.GRASS;
|
||||
}else b=B.SAND;
|
||||
}
|
||||
else if(y<=WL&&y>height)b=B.WATER;
|
||||
// caves
|
||||
if(y>1&&y<height-1&&b!==B.AIR&&b!==B.WATER){
|
||||
const cv=n3.n3(wx*.04,y*.06,wz*.04);
|
||||
if(cv>.55)b=B.AIR;
|
||||
}
|
||||
if(b!==B.AIR)ch.blocks[ch.idx(lx,y,lz)]=b;
|
||||
}
|
||||
}
|
||||
// trees
|
||||
const treeRng=new SimplexNoise(cx*9999+cz);
|
||||
for(let lz=2;lz<CS-2;lz++)for(let lx=2;lx<CS-2;lx++){
|
||||
const wx=ox+lx,wz=oz+lz;
|
||||
const biome=n2.n2(wx*.005,wz*.005);
|
||||
if(biome<.35)continue;
|
||||
const tv=treeRng.n2(wx*.5,wz*.5);
|
||||
if(tv<.6)continue;
|
||||
let surf=-1;
|
||||
for(let y=CH-1;y>WL;y--){if(ch.blocks[ch.idx(lx,y,lz)]===B.GRASS){surf=y;break;}}
|
||||
if(surf<0||surf>55)continue;
|
||||
const th=4+((tv*10|0)%4);
|
||||
// trunk
|
||||
for(let y=surf+1;y<=surf+th;y++)if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&y<CH)ch.blocks[ch.idx(lx,y,lz)]=B.WOOD;
|
||||
// leaves
|
||||
const lr=th>5?3:2;
|
||||
for(let dy=-2;dy<=1;dy++)for(let dx=-lr;dx<=lr;dx++)for(let dz=-lr;dz<=lr;dz++){
|
||||
if(dx*dx+dz*dz+dy*dy>lr*lr+1)continue;
|
||||
const tx=lx+dx,ty=surf+th+dy,tz=lz+dz;
|
||||
if(tx<0||tx>=CS||tz<0||tz>=CS||ty<0||ty>=CH)continue;
|
||||
if(ch.blocks[ch.idx(tx,ty,tz)]===B.AIR)ch.blocks[ch.idx(tx,ty,tz)]=B.LEAVES;
|
||||
}
|
||||
}
|
||||
// Apply fantasy structures
|
||||
this._applyStructures(ch,cx,cz);
|
||||
ch.gen=true;ch.dirty=true;
|
||||
this.chunks.set(this.key(cx,cz),ch);
|
||||
return ch;
|
||||
}
|
||||
_applyStructures(ch,cx,cz){
|
||||
const structures=[
|
||||
{t:'tower',x:8,z:8,r:15},
|
||||
{t:'fisland',x:65,z:45,r:12},
|
||||
{t:'fisland',x:-55,z:75,r:10},
|
||||
{t:'fisland',x:85,z:-35,r:11},
|
||||
{t:'giant_tree',x:-45,z:-35,r:10},
|
||||
{t:'ruins',x:105,z:65,r:18},
|
||||
{t:'crystal',x:-75,z:25,r:10},
|
||||
{t:'circle',x:45,z:-65,r:12},
|
||||
{t:'dragon',x:-35,z:85,r:15},
|
||||
{t:'portal',x:-90,z:-80,r:10},
|
||||
];
|
||||
const ox=cx*CS,oz=cz*CS;
|
||||
for(const s of structures){
|
||||
if(ox+CS<s.x-s.r||ox>s.x+s.r||oz+CS<s.z-s.r||oz>s.z+s.r)continue;
|
||||
const set=(wx,wy,wz,b)=>{
|
||||
const lx=wx-ox,lz=wz-oz;
|
||||
if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&wy>=0&&wy<CH){
|
||||
if(b!==B.AIR)ch.blocks[ch.idx(lx,wy,lz)]=b;
|
||||
else{ch.blocks[ch.idx(lx,wy,lz)]=B.AIR;}
|
||||
}
|
||||
};
|
||||
const get=(wx,wy,wz)=>{
|
||||
const lx=wx-ox,lz=wz-oz;
|
||||
if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&wy>=0&&wy<CH)return ch.blocks[ch.idx(lx,wy,lz)];
|
||||
return B.AIR;
|
||||
};
|
||||
if(s.t==='tower')this._tower(set,get,s.x,s.z);
|
||||
else if(s.t==='fisland')this._fisland(set,get,s.x,s.z);
|
||||
else if(s.t==='giant_tree')this._giantTree(set,get,s.x,s.z);
|
||||
else if(s.t==='ruins')this._ruins(set,get,s.x,s.z);
|
||||
else if(s.t==='crystal')this._crystal(set,get,s.x,s.z);
|
||||
else if(s.t==='circle')this._circle(set,get,s.x,s.z);
|
||||
else if(s.t==='dragon')this._dragon(set,get,s.x,s.z);
|
||||
else if(s.t==='portal')this._portal(set,get,s.x,s.z);
|
||||
}
|
||||
}
|
||||
_findSurface(x,z,get){
|
||||
for(let y=CH-1;y>0;y--){const b=get(x,y,z);if(SOLID.has(b))return y;}
|
||||
return WL;
|
||||
}
|
||||
_tower(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Base platform
|
||||
for(let dx=-6;dx<=6;dx++)for(let dz=-6;dz<=6;dz++){
|
||||
if(dx*dx+dz*dz>42)continue;
|
||||
set(cx+dx,by,cz+dz,B.MARBLE);
|
||||
for(let y=by-3;y<by;y++)set(cx+dx,y,cz+dz,B.DSTONE);
|
||||
}
|
||||
// Tower walls
|
||||
const R=4,H=55;
|
||||
for(let y=by+1;y<by+H;y++){
|
||||
const taper=1-(y-by)/H*.4;
|
||||
const r=R*taper;
|
||||
for(let a=0;a<32;a++){
|
||||
const angle=a/32*Math.PI*2;
|
||||
const tx=cx+Math.round(r*Math.cos(angle)),tz=cz+Math.round(r*Math.sin(angle));
|
||||
set(tx,y,tz,B.MARBLE);
|
||||
}
|
||||
// Windows
|
||||
if((y-by)%8===4){
|
||||
for(let a=0;a<32;a+=8){
|
||||
const angle=a/32*Math.PI*2;
|
||||
const tx=cx+Math.round((r+.5)*Math.cos(angle)),tz=cz+Math.round((r+.5)*Math.sin(angle));
|
||||
set(tx,y,tz,B.GLOW);
|
||||
}
|
||||
}
|
||||
// Spiral staircase inside
|
||||
const sa=((y-by)/2%8)/8*Math.PI*2;
|
||||
const sx=cx+Math.round(1.5*Math.cos(sa)),sz=cz+Math.round(1.5*Math.sin(sa));
|
||||
if((y-by)%2===0)set(sx,y,sz,B.DSTONE);
|
||||
}
|
||||
// Top glow
|
||||
const top=by+H;
|
||||
for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++){
|
||||
if(dx*dx+dz*dz>5)continue;
|
||||
set(cx+dx,top,cz+dz,B.CRYSTAL);
|
||||
set(cx+dx,top+1,cz+dz,B.GLOW);
|
||||
}
|
||||
// Floating crystal above tower
|
||||
for(let y=top+3;y<top+8;y++){
|
||||
set(cx,y,cz,B.CRYSTAL);
|
||||
if(y%2===0){set(cx+1,y,cz,B.CRYSTAL);set(cx-1,y,cz,B.CRYSTAL);set(cx,y,cz+1,B.CRYSTAL);set(cx,y,cz-1,B.CRYSTAL);}
|
||||
}
|
||||
}
|
||||
_fisland(set,get,cx,cz){
|
||||
const iy=52+((cx*cz)%8);
|
||||
const r=9;
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
const d2=dx*dx+dz*dz,d=Math.sqrt(d2);
|
||||
if(d>r)continue;
|
||||
const surfH=iy+Math.round(Math.cos(d/r*Math.PI/2)*3);
|
||||
// Top: grass
|
||||
set(cx+dx,surfH,cz+dz,B.GRASS);
|
||||
// Below: dirt then stone
|
||||
for(let y=surfH-3;y<surfH;y++)set(cx+dx,y,cz+dz,B.DIRT);
|
||||
for(let y=iy-8;y<surfH-3;y++){
|
||||
if(d<r-1)set(cx+dx,y,cz+dz,B.STONE);
|
||||
}
|
||||
// Bottom stalactite
|
||||
const bh=iy-8-Math.round((1-d/r)*5);
|
||||
for(let y=bh;y<iy-8;y++)if(d<r*.7)set(cx+dx,y,cz+dz,B.STONE);
|
||||
}
|
||||
// Waterfall
|
||||
for(let y=iy-20;y<iy;y++){
|
||||
set(cx+r-2,y,cz,B.WATER);
|
||||
set(cx+r-1,y,cz,B.WATER);
|
||||
}
|
||||
// Small tree on top
|
||||
const th=5;
|
||||
for(let y=iy+1;y<=iy+th;y++)set(cx,y,cz,B.WOOD);
|
||||
for(let dy=-1;dy<=1;dy++)for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++){
|
||||
if(dx*dx+dz*dz+dy*dy>6)continue;
|
||||
set(cx+dx,iy+th+dy,cz+dz,B.LEAVES);
|
||||
}
|
||||
// Glow crystals on bottom
|
||||
for(let a=0;a<6;a++){
|
||||
const ang=a/6*Math.PI*2;
|
||||
set(cx+Math.round(3*Math.cos(ang)),iy-9,cz+Math.round(3*Math.sin(ang)),B.GLOW);
|
||||
}
|
||||
}
|
||||
_giantTree(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Massive trunk
|
||||
for(let y=by;y<by+35;y++){
|
||||
const r=y<by+10?3:(y<by+25?2:1);
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
if(dx*dx+dz*dz>r*r)continue;
|
||||
set(cx+dx,y,cz+dz,B.WOOD);
|
||||
}
|
||||
}
|
||||
// Root system
|
||||
for(let a=0;a<5;a++){
|
||||
const ang=a/5*Math.PI*2;
|
||||
for(let i=1;i<8;i++){
|
||||
const rx=cx+Math.round(i*Math.cos(ang)),rz=cz+Math.round(i*Math.sin(ang));
|
||||
set(rx,by-Math.round(i*.3),rz,B.WOOD);
|
||||
set(rx,by-Math.round(i*.3)-1,rz,B.DIRT);
|
||||
}
|
||||
}
|
||||
// Giant canopy
|
||||
const top=by+35;
|
||||
for(let dy=-5;dy<=8;dy++){
|
||||
const r=dy<0?12-Math.abs(dy):Math.max(1,12-dy*1.2);
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
if(dx*dx+dz*dz>r*r)continue;
|
||||
const ty=top+dy;
|
||||
if(ty<CH&&(get(cx+dx,ty,cz+dz)===B.AIR||get(cx+dx,ty,cz+dz)===B.LEAVES))
|
||||
set(cx+dx,ty,cz+dz,B.LEAVES);
|
||||
}
|
||||
}
|
||||
// Glowing fruit
|
||||
const fr=new SimplexNoise(cx+cz*100);
|
||||
for(let i=0;i<8;i++){
|
||||
const a=i/8*Math.PI*2+fr.n2(i,i)*2;
|
||||
const d=6+((i*7)%5);
|
||||
set(cx+Math.round(d*Math.cos(a)),top-3+((i*3)%5),cz+Math.round(d*Math.sin(a)),B.GLOW);
|
||||
}
|
||||
}
|
||||
_ruins(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Stone platform
|
||||
for(let dx=-12;dx<=12;dx++)for(let dz=-8;dz<=8;dz++){
|
||||
set(cx+dx,by,cz+dz,B.COBBLE);
|
||||
if(Math.abs(dx)<=10&&Math.abs(dz)<=6)set(cx+dx,by-1,cz+dz,B.DSTONE);
|
||||
}
|
||||
// Pillars
|
||||
const px=[[-10,-6],[-10,6],[10,-6],[10,6],[0,-6],[0,6]];
|
||||
for(const[px2,pz]of px){
|
||||
const h=8+((px2*pz)%4);
|
||||
for(let y=by+1;y<by+h;y++){
|
||||
set(cx+px2,y,cz+pz,B.DSTONE);
|
||||
}
|
||||
set(cx+px2,by+h,cz+pz,B.MARBLE);
|
||||
// Lintel (broken)
|
||||
if(px2===-10||px2===10){
|
||||
for(let dx2=-1;dx2<=1;dx2++){
|
||||
const ly=by+h-1;
|
||||
set(cx+px2+dx2,ly,cz+pz,B.MARBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Inner magical circle
|
||||
for(let a=0;a<16;a++){
|
||||
const ang=a/16*Math.PI*2;
|
||||
const r=4;
|
||||
set(cx+Math.round(r*Math.cos(ang)),by+1,cz+Math.round(r*Math.sin(ang)),B.MAGIC);
|
||||
}
|
||||
// Central altar
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
set(cx+dx,by+1,cz+dz,B.MARBLE);
|
||||
set(cx+dx,by+2,cz+dz,B.MARBLE);
|
||||
}
|
||||
set(cx,by+3,cz,B.GLOW);
|
||||
}
|
||||
_crystal(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
const cr=new SimplexNoise(cx*333+cz*777);
|
||||
// Crystal spires
|
||||
for(let i=0;i<7;i++){
|
||||
const a=i/7*Math.PI*2+cr.n2(i,i)*.5;
|
||||
const d=2+((i*5)%5);
|
||||
const h=8+((i*13)%12);
|
||||
const sx=cx+Math.round(d*Math.cos(a)),sz=cz+Math.round(d*Math.sin(a));
|
||||
for(let y=by;y<by+h;y++){
|
||||
const r=Math.max(0,1.5-(y-by)/h*1.5);
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
if(dx*dx+dz*dz>r*r+1)continue;
|
||||
set(sx+dx,y,sz+dz,B.CRYSTAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Glow base
|
||||
for(let dx=-4;dx<=4;dx++)for(let dz=-4;dz<=4;dz++){
|
||||
if(dx*dx+dz*dz>16)continue;
|
||||
set(cx+dx,by,cz+dz,B.MAGIC);
|
||||
if(dx*dx+dz*dz<5)set(cx+dx,by,cz+dz,B.GLOW);
|
||||
}
|
||||
}
|
||||
_circle(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Outer ring
|
||||
const R=8;
|
||||
for(let a=0;a<24;a++){
|
||||
const ang=a/24*Math.PI*2;
|
||||
const sx=cx+Math.round(R*Math.cos(ang)),sz=cz+Math.round(R*Math.sin(ang));
|
||||
const h=3+((a*7)%4);
|
||||
for(let y=by;y<by+h;y++)set(sx,y,sz,B.COBBLE);
|
||||
set(sx,by+h,sz,B.MOSS_STONE||B.COBBLE);
|
||||
}
|
||||
// Inner stones
|
||||
for(let a=0;a<8;a++){
|
||||
const ang=a/8*Math.PI*2;
|
||||
const r2=4;
|
||||
const sx=cx+Math.round(r2*Math.cos(ang)),sz=cz+Math.round(r2*Math.sin(ang));
|
||||
for(let y=by;y<by+5;y++)set(sx,y,sz,B.DSTONE);
|
||||
set(sx,by+5,sz,B.MARBLE);
|
||||
}
|
||||
// Center altar
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
set(cx+dx,by,cz+dz,B.MARBLE);
|
||||
set(cx+dx,by+1,cz+dz,B.MARBLE);
|
||||
}
|
||||
set(cx,by+2,cz,B.GLOW);
|
||||
set(cx,by+3,cz,B.CRYSTAL);
|
||||
// Ground decoration
|
||||
for(let dx=-R;dx<=R;dx++)for(let dz=-R;dz<=R;dz++){
|
||||
const d=Math.sqrt(dx*dx+dz*dz);
|
||||
if(d<R-.5&&d>R-1.5&&get(cx+dx,by,cz+dz)===B.AIR)set(cx+dx,by,cz+dz,B.MAGIC);
|
||||
}
|
||||
}
|
||||
_dragon(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Spine
|
||||
for(let i=0;i<20;i++){
|
||||
const t=i/20;
|
||||
const x=cx+Math.round(t*25-12);
|
||||
const z=cz+Math.round(Math.sin(t*3)*4);
|
||||
const y=by+1+Math.round(Math.sin(t*Math.PI)*6);
|
||||
set(x,y,z,B.MARBLE);
|
||||
set(x,y-1,z,B.MARBLE);
|
||||
// Ribs
|
||||
if(i%3===0){
|
||||
for(let r=1;r<5;r++){
|
||||
set(x,y-r,z+r,B.MARBLE);
|
||||
set(x,y-r,z-r,B.MARBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skull
|
||||
const hx=cx-12,hy=by+7;
|
||||
for(let dx=-2;dx<=2;dx++)for(let dy=-1;dy<=2;dy++)for(let dz=-2;dz<=2;dz++){
|
||||
if(dx*dx+dy*dy*2+dz*dz>8)continue;
|
||||
set(hx+dx,hy+dy,cz+dz,B.MARBLE);
|
||||
}
|
||||
// Eye sockets
|
||||
set(hx-1,hy+1,cz-2,B.GLOW);
|
||||
set(hx-1,hy+1,cz+2,B.GLOW);
|
||||
// Jaw
|
||||
for(let dx=-2;dx<=0;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
set(hx+dx,hy-2,cz+dz,B.MARBLE);
|
||||
}
|
||||
// Teeth
|
||||
set(hx-2,hy-1,cz-1,B.CRYSTAL);
|
||||
set(hx-2,hy-1,cz+1,B.CRYSTAL);
|
||||
// Wings (simplified)
|
||||
for(let i=0;i<10;i++){
|
||||
const wx2=cx-2+i*1,wy=by+3+Math.round(Math.sin(i/10*Math.PI)*4);
|
||||
set(wx2,wy,cz-6-i,B.MARBLE);
|
||||
set(wx2,wy,cz+6+i,B.MARBLE);
|
||||
if(i%2===0){set(wx2,wy+1,cz-6-i,B.CRYSTAL);set(wx2,wy+1,cz+6+i,B.CRYSTAL);}
|
||||
}
|
||||
}
|
||||
_portal(set,get,cx,cz){
|
||||
const base=this._findSurface(cx,cz,get);
|
||||
const by=Math.max(base,WL+1);
|
||||
// Frame
|
||||
for(let y=by;y<by+10;y++){
|
||||
set(cx-3,y,cz,B.OBSIDIAN);
|
||||
set(cx+3,y,cz,B.OBSIDIAN);
|
||||
}
|
||||
for(let dx=-3;dx<=3;dx++)set(cx+dx,by+10,cz,B.OBSIDIAN);
|
||||
for(let dx=-3;dx<=3;dx++)set(cx+dx,by,cz,B.OBSIDIAN);
|
||||
// Inner glow
|
||||
for(let y=by+1;y<by+10;y++)for(let dx=-2;dx<=2;dx++){
|
||||
set(cx+dx,y,cz,B.MAGIC);
|
||||
}
|
||||
// Decorative corners
|
||||
for(let dy=-1;dy<=11;dy++){
|
||||
set(cx-4,by+dy,cz,B.DSTONE);
|
||||
set(cx+4,by+dy,cz,B.DSTONE);
|
||||
}
|
||||
set(cx-4,by+12,cz,B.GLOW);
|
||||
set(cx+4,by+12,cz,B.GLOW);
|
||||
set(cx,by+11,cz,B.CRYSTAL);
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
RAYCAST (DDA)
|
||||
====================================================== */
|
||||
function raycast(ori,dir,maxD,getBlock){
|
||||
let x=Math.floor(ori.x),y=Math.floor(ori.y),z=Math.floor(ori.z);
|
||||
const sx=dir.x>0?1:-1,sy=dir.y>0?1:-1,sz=dir.z>0?1:-1;
|
||||
const tdx=Math.abs(1/dir.x),tdy=Math.abs(1/dir.y),tdz=Math.abs(1/dir.z);
|
||||
let tmx=dir.x!==0?((dir.x>0?x+1:x)-ori.x)/dir.x:1e18;
|
||||
let tmy=dir.y!==0?((dir.y>0?y+1:y)-ori.y)/dir.y:1e18;
|
||||
let tmz=dir.z!==0?((dir.z>0?z+1:z)-ori.z)/dir.z:1e18;
|
||||
let px=x,py=y,pz=z,steps=0;
|
||||
while(steps++<maxD*2){
|
||||
const b=getBlock(x,y,z);
|
||||
if(b!==0&&b!==B.WATER)return{x,y,z,px,py,pz,block:b};
|
||||
px=x;py=y;pz=z;
|
||||
if(tmx<tmy){if(tmx<tmz){x+=sx;tmx+=tdx;}else{z+=sz;tmz+=tdz;}}
|
||||
else{if(tmy<tmz){y+=sy;tmy+=tdy;}else{z+=sz;tmz+=tdz;}}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ======================================================
|
||||
MAIN GAME
|
||||
====================================================== */
|
||||
const atlas=createAtlas();
|
||||
const world=new World(42);
|
||||
|
||||
// Three.js setup
|
||||
const renderer=new THREE.WebGLRenderer({antialias:false});
|
||||
renderer.setSize(window.innerWidth,window.innerHeight);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
|
||||
renderer.setClearColor(0x7EC8E3);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
const scene=new THREE.Scene();
|
||||
scene.fog=new THREE.Fog(0x7EC8E3,CS*2,CS*RD*.9);
|
||||
const camera=new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,.1,CS*RD*1.5);
|
||||
camera.rotation.order='YXZ';
|
||||
|
||||
const ambLight=new THREE.AmbientLight(0xffffff,.55);scene.add(ambLight);
|
||||
const hemiLight=new THREE.HemisphereLight(0x87CEEB,0x556633,.35);scene.add(hemiLight);
|
||||
const dirLight=new THREE.DirectionalLight(0xFFE8C0,.75);dirLight.position.set(60,120,40);scene.add(dirLight);
|
||||
|
||||
const mat=new THREE.MeshLambertMaterial({map:atlas.texture,side:THREE.FrontSide});
|
||||
const wmat=new THREE.MeshLambertMaterial({map:atlas.texture,transparent:true,opacity:.65,side:THREE.DoubleSide});
|
||||
|
||||
// Player state
|
||||
const player={x:8,y:50,z:8,vx:0,vy:0,vz:0,yaw:0,pitch:0,onGround:false,flying:false};
|
||||
let selectedSlot=0;
|
||||
const keys={};
|
||||
let gameStarted=false;
|
||||
const chunkQueue=new Set();
|
||||
let lastChunkKey='';
|
||||
|
||||
// UI
|
||||
const overlay=document.getElementById('overlay');
|
||||
const crosshair=document.getElementById('crosshair');
|
||||
const hotbar=document.getElementById('hotbar');
|
||||
const debugEl=document.getElementById('debug');
|
||||
|
||||
function buildHotbar(){
|
||||
hotbar.innerHTML='';
|
||||
HOTBAR_ITEMS.forEach((bt,i)=>{
|
||||
const slot=document.createElement('div');slot.className='slot'+(i===selectedSlot?' active':'');
|
||||
const num=document.createElement('span');num.className='num';num.textContent=i+1;slot.appendChild(num);
|
||||
// mini preview
|
||||
const tc=document.createElement('canvas');tc.width=16;tc.height=16;
|
||||
const tx=tc.getContext('2d');
|
||||
const faces=BF[bt];if(faces){
|
||||
const ti=faces[1]||faces[0];
|
||||
const col=ti%AC,row=ti/AC|0;
|
||||
tx.drawImage(atlas.canvas,col*TS,row*TS,TS,TS,0,0,16,16);
|
||||
}
|
||||
slot.appendChild(tc);hotbar.appendChild(slot);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHotbar(){
|
||||
hotbar.querySelectorAll('.slot').forEach((s,i)=>s.className='slot'+(i===selectedSlot?' active':''));
|
||||
}
|
||||
|
||||
function showDebug(){
|
||||
const cx=Math.floor(player.x/CS),cz=Math.floor(player.z/CS);
|
||||
debugEl.innerHTML=`XYZ: ${player.x.toFixed(1)} / ${player.y.toFixed(1)} / ${player.z.toFixed(1)}<br>Chunk: ${cx}, ${cz}<br>${player.flying?'飞行模式':'行走模式'}<br>方块: ${BN[HOTBAR_ITEMS[selectedSlot]]||'?'}`;
|
||||
}
|
||||
|
||||
// Input
|
||||
document.addEventListener('keydown',e=>{
|
||||
keys[e.code]=true;
|
||||
if(!gameStarted)return;
|
||||
if(e.code==='KeyF'){player.flying=!player.flying;player.vy=0;}
|
||||
const n=parseInt(e.key);if(n>=1&&n<=9){selectedSlot=n-1;updateHotbar();}
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('keyup',e=>keys[e.code]=false);
|
||||
document.addEventListener('wheel',e=>{
|
||||
if(!gameStarted)return;
|
||||
selectedSlot=(selectedSlot+(e.deltaY>0?1:-1)+HOTBAR_ITEMS.length)%HOTBAR_ITEMS.length;
|
||||
updateHotbar();
|
||||
});
|
||||
document.addEventListener('mousemove',e=>{
|
||||
if(!gameStarted||document.pointerLockElement!==renderer.domElement)return;
|
||||
player.yaw-=e.movementX*.002;
|
||||
player.pitch=Math.max(-Math.PI/2+.01,Math.min(Math.PI/2-.01,player.pitch-e.movementY*.002));
|
||||
});
|
||||
|
||||
// Mouse buttons
|
||||
document.addEventListener('mousedown',e=>{
|
||||
if(!gameStarted)return;
|
||||
if(e.button===0)breakBlock();
|
||||
else if(e.button===2)placeBlock();
|
||||
});
|
||||
document.addEventListener('contextmenu',e=>e.preventDefault());
|
||||
|
||||
function getViewRay(){
|
||||
const dir=new THREE.Vector3(0,0,-1);
|
||||
dir.applyEuler(new THREE.Euler(player.pitch,player.yaw,0,'YXZ'));
|
||||
return{origin:new THREE.Vector3(player.x,player.y+1.6,player.z),direction:dir.normalize()};
|
||||
}
|
||||
|
||||
function breakBlock(){
|
||||
const{origin,direction}=getViewRay();
|
||||
const hit=raycast(origin,direction,8,(x,y,z)=>world.getBlock(x,y,z));
|
||||
if(hit&&hit.block!==B.BEDROCK){world.setBlock(hit.x,hit.y,hit.z,B.AIR);}
|
||||
}
|
||||
|
||||
function placeBlock(){
|
||||
const{origin,direction}=getViewRay();
|
||||
const hit=raycast(origin,direction,8,(x,y,z)=>world.getBlock(x,y,z));
|
||||
if(!hit)return;
|
||||
const{px,py,pz}=hit;
|
||||
// Don't place inside player
|
||||
const hw=.3,h=1.8;
|
||||
if(px+1>player.x-hw&&px<player.x+hw&&py+1>player.y&&py<player.y+h&&pz+1>player.z-hw&&pz<player.z+hw)return;
|
||||
world.setBlock(px,py,pz,HOTBAR_ITEMS[selectedSlot]);
|
||||
}
|
||||
|
||||
// Collision
|
||||
function isSolid(b){return SOLID.has(b);}
|
||||
function collides(px,py,pz){
|
||||
const hw=.29,h=1.79;
|
||||
for(let x=Math.floor(px-hw);x<=Math.floor(px+hw);x++)
|
||||
for(let y=Math.floor(py);y<=Math.floor(py+h);y++)
|
||||
for(let z=Math.floor(pz-hw);z<=Math.floor(pz+hw);z++)
|
||||
if(isSolid(world.getBlock(x,y,z)))return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function updatePlayer(dt){
|
||||
if(!gameStarted)return;
|
||||
dt=Math.min(dt,.05);
|
||||
// Camera
|
||||
camera.position.set(player.x,player.y+1.6,player.z);
|
||||
camera.rotation.set(player.pitch,player.yaw,0);
|
||||
|
||||
// Movement
|
||||
const fwd=new THREE.Vector3(-Math.sin(player.yaw),0,-Math.cos(player.yaw));
|
||||
const rgt=new THREE.Vector3(Math.cos(player.yaw),0,-Math.sin(player.yaw));
|
||||
const speed=player.flying?10:6;
|
||||
let mx=0,mz=0;
|
||||
if(keys['KeyW']){mx+=fwd.x;mz+=fwd.z;}
|
||||
if(keys['KeyS']){mx-=fwd.x;mz-=fwd.z;}
|
||||
if(keys['KeyD']){mx+=rgt.x;mz+=rgt.z;}
|
||||
if(keys['KeyA']){mx-=rgt.x;mz-=rgt.z;}
|
||||
const ml=Math.sqrt(mx*mx+mz*mz);
|
||||
if(ml>.001){mx=mx/ml*speed;mz=mz/ml*speed;}
|
||||
|
||||
// Apply movement with collision (axis-separated)
|
||||
const newX=player.x+mx*dt;
|
||||
if(!collides(newX,player.y,player.z))player.x=newX;
|
||||
const newZ=player.z+mz*dt;
|
||||
if(!collides(player.x,player.y,newZ))player.z=newZ;
|
||||
|
||||
if(player.flying){
|
||||
let vy=0;
|
||||
if(keys['Space'])vy=speed;
|
||||
if(keys['ShiftLeft'])vy=-speed;
|
||||
const newY=player.y+vy*dt;
|
||||
if(!collides(player.x,newY,player.z))player.y=newY;
|
||||
player.vy=0;
|
||||
}else{
|
||||
// Gravity
|
||||
player.vy-=25*dt;
|
||||
if(player.vy<-30)player.vy=-30;
|
||||
if(keys['Space']&&player.onGround)player.vy=8;
|
||||
const newY=player.y+player.vy*dt;
|
||||
if(!collides(player.x,newY,player.z)){
|
||||
player.y=newY;player.onGround=false;
|
||||
}else{
|
||||
if(player.vy<0)player.onGround=true;
|
||||
player.vy=0;
|
||||
}
|
||||
}
|
||||
// Prevent falling into void
|
||||
if(player.y<-10){player.y=60;player.vy=0;}
|
||||
}
|
||||
|
||||
// Chunk management
|
||||
function updateChunks(){
|
||||
const pcx=Math.floor(player.x/CS),pcz=Math.floor(player.z/CS);
|
||||
const pk=pcx+','+pcz;
|
||||
// Queue new chunks
|
||||
for(let dx=-RD;dx<=RD;dx++)for(let dz=-RD;dz<=RD;dz++){
|
||||
if(dx*dx+dz*dz>RD*RD)continue;
|
||||
const cx=pcx+dx,cz=pcz+dz;
|
||||
const k=cx+','+cz;
|
||||
if(!world.chunks.has(k))chunkQueue.add(k);
|
||||
}
|
||||
// Generate a few per frame
|
||||
let count=0;
|
||||
for(const k of chunkQueue){
|
||||
if(count>=4)break;
|
||||
const[cx,cz]=k.split(',').map(Number);
|
||||
const ch=world.genChunk(cx,cz);
|
||||
ch.buildMesh((x,y,z)=>world.getBlock(x,y,z),mat,wmat,scene);
|
||||
chunkQueue.delete(k);count++;
|
||||
}
|
||||
// Rebuild dirty & visible chunks
|
||||
let rebuilds=0;
|
||||
for(const[k,ch]of world.chunks){
|
||||
const dx=ch.cx-pcx,dz=ch.cz-pcz;
|
||||
if(dx*dx+dz*dz>RD*RD+2){ch.dispose(scene);world.chunks.delete(k);continue;}
|
||||
if(ch.dirty&&rebuilds<4){
|
||||
ch.buildMesh((x,y,z)=>world.getBlock(x,y,z),mat,wmat,scene);
|
||||
rebuilds++;
|
||||
}
|
||||
}
|
||||
if(pk!==lastChunkKey){lastChunkKey=pk;showDebug();}
|
||||
}
|
||||
|
||||
// Events
|
||||
overlay.addEventListener('click',()=>{
|
||||
renderer.domElement.requestPointerLock();
|
||||
});
|
||||
document.addEventListener('pointerlockchange',()=>{
|
||||
if(document.pointerLockElement===renderer.domElement){
|
||||
gameStarted=true;overlay.classList.add('hidden');
|
||||
crosshair.style.display='block';hotbar.style.display='flex';debugEl.style.display='block';
|
||||
buildHotbar();
|
||||
}else{
|
||||
if(gameStarted){overlay.classList.remove('hidden');overlay.querySelector('.start').textContent='— 点击继续探险 —';}
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize',()=>{
|
||||
camera.aspect=window.innerWidth/window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth,window.innerHeight);
|
||||
});
|
||||
|
||||
// Find spawn
|
||||
function findSpawnY(){
|
||||
for(let y=CH-1;y>0;y--){
|
||||
if(isSolid(world.getBlock(8,y,8)))return y+1;
|
||||
}
|
||||
return 40;
|
||||
}
|
||||
|
||||
// Init
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)world.genChunk(dx,dz);
|
||||
player.y=findSpawnY();
|
||||
|
||||
// Game loop
|
||||
let lastTime=performance.now();
|
||||
function gameLoop(now){
|
||||
requestAnimationFrame(gameLoop);
|
||||
const dt=(now-lastTime)/1000;lastTime=now;
|
||||
updatePlayer(dt);
|
||||
updateChunks();
|
||||
renderer.render(scene,camera);
|
||||
if(gameStarted&&Math.floor(now/500)%2===0)showDebug();
|
||||
}
|
||||
requestAnimationFrame(gameLoop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,590 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mythical Voxel Realm</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{overflow:hidden;background:#000;font-family:'Segoe UI',sans-serif}
|
||||
canvas{display:block}
|
||||
#splash{position:fixed;inset:0;background:radial-gradient(ellipse at 50% 40%,#1e0a3e 0%,#0a0520 50%,#050210 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;cursor:pointer}
|
||||
#splash h1{font-family:Georgia,serif;font-size:clamp(28px,6vw,58px);font-weight:700;background:linear-gradient(135deg,#c084fc,#22d3ee,#a78bfa,#f472b6);background-size:300% 300%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;animation:g4s ease infinite;margin-bottom:10px;text-align:center}
|
||||
@keyframes g{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
|
||||
#splash .sub{font-family:Georgia,serif;font-size:clamp(14px,2vw,20px);color:#6b5b8a;font-style:italic;margin-bottom:40px}
|
||||
#splash .play{font-family:monospace;font-size:15px;color:#c4b5fd;padding:14px 40px;border:1px solid #c4b5fd;border-radius:6px;letter-spacing:2px;text-transform:uppercase;transition:all .3s}
|
||||
#splash .play:hover{background:rgba(196,181,253,.1);box-shadow:0 0 20px rgba(196,181,253,.2)}
|
||||
#splash .ctrls{margin-top:36px;color:#4a3d6b;font-family:monospace;font-size:12px;text-align:center;line-height:2}
|
||||
#ui{position:fixed;inset:0;pointer-events:none;z-index:10}
|
||||
#cross{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px}
|
||||
#cross::before,#cross::after{content:'';position:absolute;background:rgba(255,255,255,.85)}
|
||||
#cross::before{left:50%;top:2px;width:2px;height:20px;transform:translateX(-50%)}
|
||||
#cross::after{top:50%;left:2px;height:2px;width:20px;transform:translateY(-50%)}
|
||||
#bar{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);display:flex;gap:2px;background:rgba(0,0,0,.55);padding:4px;border-radius:4px;border:2px solid rgba(255,255,255,.15)}
|
||||
.sl{width:44px;height:44px;border:2px solid rgba(255,255,255,.12);background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;position:relative;border-radius:2px;transition:border-color .15s}
|
||||
.sl.a{border-color:rgba(255,255,255,.85);background:rgba(255,255,255,.08)}
|
||||
.sl canvas{width:32px;height:32px;image-rendering:pixelated}
|
||||
.sl .n{position:absolute;top:1px;left:3px;font-size:9px;color:rgba(255,255,255,.4);font-family:monospace}
|
||||
#dbg{position:absolute;top:8px;left:8px;font-size:12px;color:#fff;font-family:monospace;text-shadow:1px 1px 2px #000;line-height:1.6}
|
||||
#bname{position:absolute;bottom:68px;left:50%;transform:translateX(-50%);color:#fff;font-family:monospace;font-size:13px;text-shadow:1px 1px 3px #000}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="splash"><h1>Mythical Voxel Realm</h1><div class="sub">Explore ancient magic and forgotten realms</div><div class="play">Click to Enter</div><div class="ctrls">WASD — Move | Mouse — Look | Space — Jump | Shift — Sprint<br>Left Click — Break Block | Right Click — Place Block<br>1–9 / Scroll — Select Block | ESC — Release Cursor</div></div>
|
||||
<div id="ui"><div id="cross"></div><div id="bar"></div><div id="dbg"></div><div id="bname"></div></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.157.0/build/three.min.js"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
// ─── SIMPLEX NOISE ─────────────────────────────────────────
|
||||
class SN{constructor(s=Math.random()){this.g=[[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]];this.p=new Uint8Array(256);for(let i=0;i<256;i++)this.p[i]=i;let v=s*2147483647;for(let i=255;i>0;i--){v=(v*16807)%2147483647;const j=v%(i+1);[this.p[i],this.p[j]]=[this.p[j],this.p[i]]}this.pp=new Uint8Array(512);for(let i=0;i<512;i++)this.pp[i]=this.p[i&255]}
|
||||
n(x,y){const F=.5*(Math.sqrt(3)-1),G=(3-Math.sqrt(3))/6,s=(x+y)*F,i=Math.floor(x+s),j=Math.floor(y+s),t=(i+j)*G,x0=x-(i-t),y0=y-(j+t-t),i1=x0>y0?1:0,j1=x0>y0?0:1,x1=x0-i1+G,y1=y0-j1+G,x2=x0-1+2*G,y2=y0-1+2*G,ii=i&255,jj=j&255,pp=this.pp,g=this.g;let gi0=pp[ii+pp[jj]]&7,gi1=pp[ii+i1+pp[jj+j1]]&7,gi2=pp[ii+1+pp[jj+1]]&7,n0=0,n1=0,n2=0;let t0=.5-x0*x0-y0*y0;if(t0>=0){t0*=t0;n0=t0*t0*(g[gi0][0]*x0+g[gi0][1]*y0)}let t1=.5-x1*x1-y1*y1;if(t1>=0){t1*=t1;n1=t1*t1*(g[gi1][0]*x1+g[gi1][1]*y1)}let t2=.5-x2*x2-y2*y2;if(t2>=0){t2*=t2;n2=t2*t2*(g[gi2][0]*x2+g[gi2][1]*y2)}return 70*(n0+n1+n2)}}
|
||||
|
||||
const ns=new SN(12345);
|
||||
function fbm(x,y,oct,pers,sc){let v=0,a=1,f=sc,m=0;for(let i=0;i<oct;i++){v+=ns.n(x*f,y*f)*a;m+=a;a*=pers;f*=2}return v/m}
|
||||
|
||||
// ─── BLOCKS & CONSTANTS ────────────────────────────────────
|
||||
const CS=16,CH=96,RD=5,AS=5,TS=16;
|
||||
const B={AIR:0,GRASS:1,DIRT:2,STONE:3,SAND:4,WATER:5,WOOD:6,LEAVES:7,CRYSTAL:8,COBBLE:9,PLANKS:10,BRICK:11,GLOW:12,OBSIDIAN:13,SNOW:14,GOLD:15,MSTONE:16,ANCIENT:17,ENCHANTED:18,MOSSY:19};
|
||||
const BN={[B.GRASS]:'Grass',[B.DIRT]:'Dirt',[B.STONE]:'Stone',[B.COBBLE]:'Cobble',[B.WOOD]:'Wood',[B.PLANKS]:'Planks',[B.CRYSTAL]:'Crystal',[B.BRICK]:'Brick',[B.GLOW]:'Glowstone',[B.SAND]:'Sand',[B.OBSIDIAN]:'Obsidian',[B.SNOW]:'Snow',[B.GOLD]:'Gold',[B.MSTONE]:'Mystic Stone',[B.ANCIENT]:'Ancient Stone',[B.ENCHANTED]:'Enchanted',[B.MOSSY]:'Mossy Stone',[B.LEAVES]:'Leaves'};
|
||||
const HB=[B.GRASS,B.DIRT,B.STONE,B.COBBLE,B.WOOD,B.PLANKS,B.CRYSTAL,B.BRICK,B.GLOW];
|
||||
const TPOS={gt:[0,0],gs:[1,0],dt:[2,0],st:[3,0],sa:[4,0],wt:[0,1],ws:[1,1],we:[2,1],lv:[3,1],cr:[4,1],cb:[0,2],pk:[1,2],br:[2,2],gw:[3,2],ob:[4,2],sn:[0,3],gd:[1,3],ms:[2,3],an:[3,3],en:[4,3],my:[0,4]};
|
||||
const BTEX={[B.GRASS]:{t:TPOS.gt,b:TPOS.dt,s:TPOS.gs},[B.DIRT]:{t:TPOS.dt,b:TPOS.dt,s:TPOS.dt},[B.STONE]:{t:TPOS.st,b:TPOS.st,s:TPOS.st},[B.SAND]:{t:TPOS.sa,b:TPOS.sa,s:TPOS.sa},[B.WATER]:{t:TPOS.wt,b:TPOS.wt,s:TPOS.wt},[B.WOOD]:{t:TPOS.we,b:TPOS.we,s:TPOS.ws},[B.LEAVES]:{t:TPOS.lv,b:TPOS.lv,s:TPOS.lv},[B.CRYSTAL]:{t:TPOS.cr,b:TPOS.cr,s:TPOS.cr},[B.COBBLE]:{t:TPOS.cb,b:TPOS.cb,s:TPOS.cb},[B.PLANKS]:{t:TPOS.pk,b:TPOS.pk,s:TPOS.pk},[B.BRICK]:{t:TPOS.br,b:TPOS.br,s:TPOS.br},[B.GLOW]:{t:TPOS.gw,b:TPOS.gw,s:TPOS.gw},[B.OBSIDIAN]:{t:TPOS.ob,b:TPOS.ob,s:TPOS.ob},[B.SNOW]:{t:TPOS.sn,b:TPOS.sn,s:TPOS.sn},[B.GOLD]:{t:TPOS.gd,b:TPOS.gd,s:TPOS.gd},[B.MSTONE]:{t:TPOS.ms,b:TPOS.ms,s:TPOS.ms},[B.ANCIENT]:{t:TPOS.an,b:TPOS.an,s:TPOS.an},[B.ENCHANTED]:{t:TPOS.en,b:TPOS.en,s:TPOS.en},[B.MOSSY]:{t:TPOS.my,b:TPOS.my,s:TPOS.my}};
|
||||
const TR=new Set([B.AIR,B.WATER,B.LEAVES]);
|
||||
const FACES=[
|
||||
{d:[0,1,0],c:[[0,1,1,0,1],[1,1,1,1,1],[1,1,0,1,0],[0,1,0,0,0]],bri:1},
|
||||
{d:[0,-1,0],c:[[0,0,0,0,0],[1,0,0,1,0],[1,0,1,1,1],[0,0,1,0,1]],bri:.5},
|
||||
{d:[0,0,1],c:[[0,0,1,0,0],[1,0,1,1,0],[1,1,1,1,1],[0,1,1,0,1]],bri:.75},
|
||||
{d:[0,0,-1],c:[[1,0,0,0,0],[0,0,0,1,0],[0,1,0,1,1],[1,1,0,0,1]],bri:.75},
|
||||
{d:[1,0,0],c:[[1,0,1,0,0],[1,0,0,1,0],[1,1,0,1,1],[1,1,1,0,1]],bri:.6},
|
||||
{d:[-1,0,0],c:[[0,0,0,0,0],[0,0,1,1,0],[0,1,1,1,1],[0,1,0,0,1]],bri:.6}];
|
||||
|
||||
// ─── TEXTURE ATLAS ─────────────────────────────────────────
|
||||
function mkAtlas(){
|
||||
const cv=document.createElement('canvas');const S=AS*TS;cv.width=cv.height=S;const x=cv.getContext('2d');
|
||||
function R(s){let v=s;return()=>{v=(v*1103515245+12345)&0x7fffffff;return v/0x7fffffff}}
|
||||
function dt(col,row,fn){x.save();x.translate(col*TS,row*TS);x.beginPath();x.rect(0,0,TS,TS);x.clip();fn(x,R(col*997+row*991+7));x.restore()}
|
||||
function px(x2,r,colors){for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();x2.fillStyle=colors[v>colors.length?0:Math.floor(v*colors.length)];x2.fillRect(i,j,1,1)}}
|
||||
// grass top
|
||||
dt(...TPOS.gt,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#3d7a22':v>.3?'#4a8c2a':'#55a030';c.fillRect(i,j,1,1)}});
|
||||
// grass side
|
||||
dt(...TPOS.gs,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.6?'#7a5c12':v>.3?'#8b6914':'#9a7520';c.fillRect(i,j,1,1)}for(let i=0;i<TS;i++){const h=3+(r()>.5?1:0);for(let j=0;j<h;j++){c.fillStyle=r()>.5?'#4a8c2a':'#55a030';c.fillRect(i,j,1,1)}}});
|
||||
// dirt
|
||||
dt(...TPOS.dt,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#7a5c12':v>.3?'#8b6914':'#9a7520';c.fillRect(i,j,1,1)}});
|
||||
// stone
|
||||
dt(...TPOS.st,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.7?'#686868':v>.35?'#808080':'#737373';c.fillRect(i,j,1,1)}c.fillStyle='#5a5a5a';c.fillRect(3,4,5,1);c.fillRect(8,10,4,1);c.fillRect(1,12,3,1)});
|
||||
// sand
|
||||
dt(...TPOS.sa,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#e8d090':v>.3?'#dbc480':'#d0b870';c.fillRect(i,j,1,1)}});
|
||||
// water
|
||||
dt(...TPOS.wt,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.6?'#2060b0':v>.2?'#3070c0':'#2868b8';c.fillRect(i,j,1,1)}c.fillStyle='rgba(120,180,255,.35)';c.fillRect(3,2,5,1);c.fillRect(10,8,3,1);c.fillRect(1,13,4,1)});
|
||||
// wood side
|
||||
dt(...TPOS.ws,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#5a3520':v>.3?'#6b4226':'#7a4f30';c.fillRect(i,j,1,1)}c.fillStyle='#4a2a18';for(let j=0;j<TS;j+=3)c.fillRect(0,j,TS,1)});
|
||||
// wood end
|
||||
dt(...TPOS.we,(c,r)=>{c.fillStyle='#8a6840';c.fillRect(0,0,TS,TS);c.fillStyle='#6b4830';c.beginPath();c.arc(8,8,6,0,Math.PI*2);c.lineWidth=1.5;c.strokeStyle='#5a3520';c.stroke();c.fillStyle='#9a7850';c.beginPath();c.arc(8,8,2,0,Math.PI*2);c.fill()});
|
||||
// leaves
|
||||
dt(...TPOS.lv,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();if(v>.12){c.fillStyle=v>.6?'#2d6b1a':v>.3?'#3a8020':'#459028';c.fillRect(i,j,1,1)}}});
|
||||
// crystal
|
||||
dt(...TPOS.cr,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#9070e8':v>.3?'#6848c8':'#7858d8';c.fillRect(i,j,1,1)}c.fillStyle='#d0b8ff';c.fillRect(4,3,2,2);c.fillRect(10,8,2,2);c.fillRect(2,11,2,1)});
|
||||
// cobblestone
|
||||
dt(...TPOS.cb,(c,r)=>{c.fillStyle='#606060';c.fillRect(0,0,TS,TS);
|
||||
[[0,0,6,4],[6,0,5,4],[11,0,5,4],[0,5,5,5],[5,5,6,5],[11,5,5,5],[0,10,5,6],[5,11,5,5],[10,10,6,6]].forEach(([bx,by,bw,bh])=>{c.fillStyle=r()>.5?'#707070':'#7a7a7a';c.fillRect(bx+1,by+1,bw-2,bh-2);c.fillStyle='#505050';c.fillRect(bx,by,bw,1);c.fillRect(bx,by,1,bh)})});
|
||||
// planks
|
||||
dt(...TPOS.pk,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.6?'#b08850':v>.3?'#c09860':'#b89058';c.fillRect(i,j,1,1)}c.fillStyle='#7a5a30';c.fillRect(0,3,TS,1);c.fillRect(0,7,TS,1);c.fillRect(0,11,TS,1);c.fillRect(0,15,TS,1)});
|
||||
// brick
|
||||
dt(...TPOS.br,(c,r)=>{c.fillStyle='#555';c.fillRect(0,0,TS,TS);
|
||||
[[0,0,7,3],[8,0,8,3],[0,4,4,3],[5,4,6,3],[12,4,4,3],[0,8,7,3],[8,8,8,3],[0,12,4,3],[5,12,6,3],[12,12,4,3]].forEach(([bx,by,bw,bh])=>{c.fillStyle=r()>.5?'#a04030':'#904038';c.fillRect(bx,by,bw,bh)})});
|
||||
// glowstone
|
||||
dt(...TPOS.gw,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#ffdd44':v>.3?'#eec030':'#ddaa20';c.fillRect(i,j,1,1)}c.fillStyle='#fff8cc';c.fillRect(3,3,3,3);c.fillRect(10,9,3,3)});
|
||||
// obsidian
|
||||
dt(...TPOS.ob,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.75?'#2a1a3a':v>.35?'#1a0a2a':'#120820';c.fillRect(i,j,1,1)}c.fillStyle='#3a2050';c.fillRect(5,2,3,1);c.fillRect(10,7,3,1);c.fillRect(1,12,3,1)});
|
||||
// snow
|
||||
dt(...TPOS.sn,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#f0f0f8':v>.3?'#e8e8f0':'#e0e0e8';c.fillRect(i,j,1,1)}});
|
||||
// gold
|
||||
dt(...TPOS.gd,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#ffd700':v>.3?'#daa520':'#c49b18';c.fillRect(i,j,1,1)}});
|
||||
// mystic stone
|
||||
dt(...TPOS.ms,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.6?'#4a3870':v>.3?'#3a2860':'#2a1850';c.fillRect(i,j,1,1)}c.fillStyle='#8060c0';c.fillRect(4,4,2,2);c.fillRect(10,10,2,2);c.fillRect(7,1,2,1)});
|
||||
// ancient
|
||||
dt(...TPOS.an,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#8a8070':v>.3?'#7a7060':'#6a6050';c.fillRect(i,j,1,1)}c.fillStyle='#5a5040';c.fillRect(0,4,TS,1);c.fillRect(0,11,TS,1);c.fillRect(4,0,1,TS);c.fillRect(11,0,1,TS)});
|
||||
// enchanted
|
||||
dt(...TPOS.en,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.6?'#1a6060':v>.3?'#145050':'#104040';c.fillRect(i,j,1,1)}c.fillStyle='#40e0c0';c.fillRect(7,7,2,2);c.fillRect(2,3,1,1);c.fillRect(12,10,1,1);c.fillRect(5,13,1,1)});
|
||||
// mossy
|
||||
dt(...TPOS.my,(c,r)=>{for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){const v=r();c.fillStyle=v>.65?'#666':v>.3?'#777':'#6a6a6a';c.fillRect(i,j,1,1)}for(let i=0;i<TS;i++)for(let j=0;j<TS;j++){if(r()>.65){c.fillStyle=r()>.5?'#3a6a2a':'#4a7a3a';c.fillRect(i,j,1,1)}}});
|
||||
return cv}
|
||||
|
||||
// ─── THREE.JS SETUP ────────────────────────────────────────
|
||||
const scene=new THREE.Scene();
|
||||
scene.background=new THREE.Color(0x7EC8E3);
|
||||
scene.fog=new THREE.Fog(0x7EC8E3,50,95);
|
||||
const camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,.1,200);
|
||||
const renderer=new THREE.WebGLRenderer({antialias:false});
|
||||
renderer.setSize(innerWidth,innerHeight);
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
|
||||
document.body.appendChild(renderer.domElement);
|
||||
const amb=new THREE.AmbientLight(0x8899bb,.55);scene.add(amb);
|
||||
const sun=new THREE.DirectionalLight(0xfff5e0,.85);sun.position.set(100,200,80);scene.add(sun);
|
||||
const hem=new THREE.HemisphereLight(0x87CEEB,0x556633,.35);scene.add(hem);
|
||||
|
||||
const atlasCanvas=mkAtlas();
|
||||
const atlasTex=new THREE.CanvasTexture(atlasCanvas);
|
||||
atlasTex.magFilter=THREE.NearestFilter;atlasTex.minFilter=THREE.NearestFilter;
|
||||
atlasTex.colorSpace=THREE.SRGBColorSpace;
|
||||
const blockMat=new THREE.MeshLambertMaterial({map:atlasTex,vertexColors:true,alphaTest:.3,side:THREE.FrontSide});
|
||||
|
||||
// ─── HIGHLIGHT CUBE ────────────────────────────────────────
|
||||
const hlGeo=new THREE.BoxGeometry(1.005,1.005,1.005);
|
||||
const hlMat=new THREE.MeshBasicMaterial({color:0x000000,wireframe:true,transparent:true,opacity:.45});
|
||||
const hlMesh=new THREE.Mesh(hlGeo,hlMat);hlMesh.visible=false;scene.add(hlMesh);
|
||||
|
||||
// ─── WORLD ─────────────────────────────────────────────────
|
||||
const chunks=new Map();
|
||||
const WATER_L=32;
|
||||
const pendingMeshes=[];
|
||||
|
||||
function chunkKey(cx,cz){return cx+','+cz}
|
||||
function getTH(wx,wz){return Math.floor(38+fbm(wx,wz,5,.45,.008)*28+fbm(wx,wz,3,.4,.03)*8)}
|
||||
|
||||
function genTree(wx,baseY,wz,blocks){
|
||||
const h=4+Math.floor(Math.abs(ns.n(wx*.37,wz*.37))*3);
|
||||
for(let y=0;y<h;y++)blocks.set(wx,baseY+y,wz,B.WOOD);
|
||||
const r=2;const top=baseY+h;
|
||||
for(let dy=0;dy<=r;dy++){
|
||||
const rad=dy===0?r:dy===r?1:2;
|
||||
for(let dx=-rad;dx<=rad;dx++)for(let dz=-rad;dz<=rad;dz++){
|
||||
if(dx*dx+dz*dz<=rad*rad+1&&!(dx===0&&dz===0&&dy<r)){
|
||||
blocks.set(wx+dx,top+dy,wz+dz,B.LEAVES)}}}}
|
||||
|
||||
function genChunk(cx,cz){
|
||||
const key=chunkKey(cx,cz);
|
||||
if(chunks.has(key))return chunks.get(key);
|
||||
const data=new Uint8Array(CS*CH*CS);
|
||||
const wx0=cx*CS,wz0=cz*CS;
|
||||
for(let lx=0;lx<CS;lx++)for(let lz=0;lz<CS;lz++){
|
||||
const wx=wx0+lx,wz=wz0+lz;
|
||||
const h=getTH(wx,wz);
|
||||
for(let y=0;y<CH&&y<=h;y++){
|
||||
let b=B.STONE;
|
||||
if(y===h){
|
||||
if(h<WATER_L+2)b=B.SAND;
|
||||
else if(h>65)b=B.SNOW;
|
||||
else b=B.GRASS;
|
||||
}else if(y>h-4&&h>WATER_L+2)b=B.DIRT;
|
||||
data[lx*CH*CS+lz*CH+y]=b}
|
||||
for(let y=h+1;y<=WATER_L;y++)data[lx*CH*CS+lz*CH+y]=B.WATER;
|
||||
// trees
|
||||
if(h>WATER_L+3&&h<62){
|
||||
const tv=ns.n(wx*.5+.1,wz*.5+.1);
|
||||
if(tv>.72&&lx>2&&lx<CS-3&&lz>2&&lz<CS-3){
|
||||
genTree(wx,h+1,wz,{set:(x,y,z,t)=>{const ax=x-wx0,az=z-wz0;if(ax>=0&&ax<CS&&az>=0&&az<CS&&y>=0&&y<CH)data[ax*CH*CS+az*CH+y]=t}})}}}
|
||||
const ch={cx,cz,data,mesh:null,dirty:true};
|
||||
chunks.set(key,ch);
|
||||
return ch}
|
||||
|
||||
function getBlock(wx,wy,wz){
|
||||
if(wy<0||wy>=CH)return B.AIR;
|
||||
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
|
||||
const ch=chunks.get(chunkKey(cx,cz));
|
||||
if(!ch)return B.AIR;
|
||||
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
|
||||
return ch.data[lx*CH*CS+lz*CH+wy]}
|
||||
|
||||
function setBlock(wx,wy,wz,t){
|
||||
if(wy<0||wy>=CH)return;
|
||||
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
|
||||
const key=chunkKey(cx,cz);
|
||||
const ch=chunks.get(key);if(!ch)return;
|
||||
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
|
||||
ch.data[lx*CH*CS+lz*CH+wy]=t;
|
||||
ch.dirty=true;
|
||||
if(lx===0)markDirty(cx-1,cz);
|
||||
if(lx===CS-1)markDirty(cx+1,cz);
|
||||
if(lz===0)markDirty(cx,cz-1);
|
||||
if(lz===CS-1)markDirty(cx,cz+1)}
|
||||
function markDirty(cx,cz){const ch=chunks.get(chunkKey(cx,cz));if(ch)ch.dirty=true}
|
||||
|
||||
function buildMesh(ch){
|
||||
const{cx,cz}=ch;
|
||||
const wx0=cx*CS,wz0=cz*CS;
|
||||
const pos=[],norms=[],uvs=[],cols=[],idx=[];
|
||||
let vc=0;
|
||||
for(let lx=0;lx<CS;lx++)for(let ly=0;ly<CH;ly++)for(let lz=0;lz<CS;lz++){
|
||||
const b=ch.data[lx*CH*CS+lz*CH+ly];
|
||||
if(b===B.AIR)continue;
|
||||
const wx=wx0+lx,wz=wz0+lz;
|
||||
const bt=BTEX[b];if(!bt)continue;
|
||||
for(let fi=0;fi<6;fi++){
|
||||
const f=FACES[fi];const[dx,dy,dz]=f.d;
|
||||
const nb=getBlock(wx+dx,ly+dy,wz+dz);
|
||||
if(!TR.has(nb)&&(nb!==B.AIR))continue;
|
||||
let tc;
|
||||
if(fi===0)tc=bt.t;else if(fi===1)tc=bt.b;else tc=bt.s;
|
||||
const[col,row]=tc;
|
||||
for(const v of f.c){
|
||||
pos.push(wx+v[0],ly+v[1],wz+v[2]);
|
||||
norms.push(f.d[0],f.d[1],f.d[2]);
|
||||
uvs.push((col+v[3])/AS,(AS-1-row+v[4])/AS);
|
||||
const bri=f.bri;cols.push(bri,bri,bri)}
|
||||
idx.push(vc,vc+1,vc+2,vc,vc+2,vc+3);vc+=4}}
|
||||
if(pos.length===0)return null;
|
||||
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(idx);
|
||||
return new THREE.Mesh(geo,blockMat)}
|
||||
|
||||
// ─── FANTASY STRUCTURES ────────────────────────────────────
|
||||
function place(cx,cz,wx,wy,wz,b){
|
||||
const ax=wx-cx*CS,az=wz-cz*CS;
|
||||
if(ax<0||ax>=CS||az<0||az>=CS||wy<0||wy>=CH)return;
|
||||
cx;cz;const ch=chunks.get(chunkKey(cx,cz));
|
||||
if(ch)ch.data[ax*CH*CS+az*CH+wy]=b;ch.dirty=true}
|
||||
|
||||
function spiral(wx,wz,h,b,world){
|
||||
for(let y=0;y<h;y++){
|
||||
const r=Math.max(1,4-Math.floor(y/10));
|
||||
const angle=y*.4;
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
if(dx*dx+dz*dz<=r*r){
|
||||
const bx=wx+Math.round(dx*Math.cos(angle)-dz*Math.sin(angle));
|
||||
const bz=wz+Math.round(dx*Math.sin(angle)+dz*Math.cos(angle));
|
||||
const cx2=Math.floor(bx/CS),cz2=Math.floor(bz/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=((bx%CS)+CS)%CS,lz=((bz%CS)+CS)%CS;if(lz>=0&&lz<CS&&y>=0&&y<CH)ch.data[lx*CH*CS+lz*CH+y]=b;ch.dirty=true}}}}}
|
||||
|
||||
function crystalSpire(wx,wz){
|
||||
const baseY=getTH(wx,wz);
|
||||
const h=50;
|
||||
for(let y=0;y<h;y++){
|
||||
const r=Math.max(1,Math.floor(5-y/12));
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
if(dx*dx+dz*dz<=r*r){
|
||||
const by=baseY+y;const b=y%7===0?B.GLOW:B.CRYSTAL;
|
||||
const cx2=Math.floor((wx+dx)/CS),cz2=Math.floor((wz+dz)/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=((wx+dx)%CS+CS)%CS,lz=((wz+dz)%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&by>=0&&by<CH)ch.data[lx*CH*CS+lz*CH+by]=b;ch.dirty=true}}}}}
|
||||
|
||||
function ancientPortal(wx,wz){
|
||||
const baseY=getTH(wx,wz);
|
||||
const r=9;
|
||||
for(let a=0;a<Math.PI*2;a+=.45){
|
||||
const px2=Math.round(wx+r*Math.cos(a)),pz2=Math.round(wz+r*Math.sin(a));
|
||||
for(let dy=0;dy<12;dy++){
|
||||
const b=dy===11?B.MSTONE:B.ANCIENT;
|
||||
const cx2=Math.floor(px2/CS),cz2=Math.floor(pz2/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(px2%CS+CS)%CS,lz=(pz2%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&baseY+dy>=0&&baseY+dy<CH){ch.data[lx*CH*CS+lz*CH+baseY+dy]=b;ch.dirty=true}}}
|
||||
// cap stones
|
||||
const px3=Math.round(wx+r*Math.cos(a)),pz3=Math.round(wz+r*Math.sin(a));
|
||||
for(let d=-1;d<=1;d++){
|
||||
const cx2=Math.floor(px3/CS),cz2=Math.floor((pz3+d)/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(px3%CS+CS)%CS,lz=((pz3+d)%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&baseY+11>=0&&baseY+11<CH){ch.data[lx*CH*CS+lz*CH+baseY+11]=B.MSTONE;ch.dirty=true}}}}
|
||||
// glow center
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
const cx2=Math.floor((wx+dx)/CS),cz2=Math.floor((wz+dz)/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=((wx+dx)%CS+CS)%CS,lz=((wz+dz)%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&baseY>=0&&baseY<CH){ch.data[lx*CH*CS+lz*CH+baseY]=B.GLOW;ch.dirty=true}}}}
|
||||
|
||||
function wizardTower(wx,wz){
|
||||
const baseY=getTH(wx,wz);
|
||||
for(let y=0;y<38;y++){
|
||||
const r=Math.max(2,5-Math.floor(y/10));
|
||||
const offX=Math.floor(Math.sin(y*.08)*2);
|
||||
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
|
||||
const bx=wx+dx+offX,bz=wz+dz;
|
||||
if(dx*dx+dz*dz<=r*r){
|
||||
const isEdge=dx*dx+dz*dz>(r-1)*(r-1);
|
||||
const b=isEdge?B.ANCIENT:B.MSTONE;
|
||||
const by=baseY+y;
|
||||
const cx2=Math.floor(bx/CS),cz2=Math.floor(bz/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(bx%CS+CS)%CS,lz=(bz%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&by>=0&&by<CH)ch.data[lx*CH*CS+lz*CH+by]=b;ch.dirty=true}}}}
|
||||
// glow orb
|
||||
const orbY=baseY+38;
|
||||
for(let dx=-1;dx<=1;dx++)for(let dy=-1;dy<=1;dy++)for(let dz=-1;dz<=1;dz++){
|
||||
if(dx*dx+dy*dy+dz*dz<=2){
|
||||
const bx=wx+dx+3,bz=wz+dz;
|
||||
const cx2=Math.floor(bx/CS),cz2=Math.floor(bz/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(bx%CS+CS)%CS,lz=(bz%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&orbY+dy>=0&&orbY+dy<CH){ch.data[lx*CH*CS+lz*CH+orbY+dy]=B.GLOW;ch.dirty=true}}}}}
|
||||
|
||||
function floatingIsland(wx,wz){
|
||||
const floatY=75;
|
||||
// Pillar
|
||||
const baseY=getTH(wx,wz);
|
||||
for(let y=baseY;y<floatY;y++){
|
||||
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
|
||||
const cx2=Math.floor((wx+dx)/CS),cz2=Math.floor((wz+dz)/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=((wx+dx)%CS+CS)%CS,lz=((wz+dz)%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&y>=0&&y<CH){ch.data[lx*CH*CS+lz*CH+y]=B.OBSIDIAN;ch.dirty=true}}}}
|
||||
// Island body
|
||||
for(let dy=-4;dy<=3;dy++)for(let dx=-7;dx<=7;dx++)for(let dz=-7;dz<=7;dz++){
|
||||
const d2=dx*dx+dz*dz+dy*dy*4;
|
||||
if(d2<45){
|
||||
const by=floatY+dy;const bx=wx+dx,bz=wz+dz;
|
||||
let b=B.DIRT;
|
||||
if(dy===3)b=B.GRASS;
|
||||
if(dy===3&&d2<5)b=B.ENCHANTED;
|
||||
const cx2=Math.floor(bx/CS),cz2=Math.floor(bz/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(bx%CS+CS)%CS,lz=(bz%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&by>=0&&by<CH){ch.data[lx*CH*CS+lz*CH+by]=b;ch.dirty=true}}}}
|
||||
// Enchanted trees
|
||||
for(const [tx,tz] of [[3,2],[-3,-2],[0,4]]){
|
||||
const ty=floatY+4;
|
||||
for(let y=0;y<5;y++){
|
||||
const cx2=Math.floor((wx+tx)/CS),cz2=Math.floor((wz+tz)/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=((wx+tx)%CS+CS)%CS,lz=((wz+tz)%CS+CS)%CS;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&ty+y>=0&&ty+y<CH){ch.data[lx*CH*CS+lz*CH+ty+y]=B.WOOD;ch.dirty=true}}}
|
||||
for(let dx2=-2;dx2<=2;dx2++)for(let dz2=-2;dz2<=2;dz2++)for(let dy2=0;dy2<=2;dy2++){
|
||||
if(dx2*dx2+dz2*dz2+dy2*dy2<6){
|
||||
const bx2=wx+tx+dx2,bz2=wz+tz+dz2;
|
||||
const cx2=Math.floor(bx2/CS),cz2=Math.floor(bz2/CS);
|
||||
const ch=chunks.get(chunkKey(cx2,cz2));
|
||||
if(ch){const lx=(bx2%CS+CS)%CS,lz=(bz2%CS+CS)%CS;const by2=ty+5+dy2;if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&by2>=0&&by2<CH){ch.data[lx*CH*CS+lz*CH+by2]=B.ENCHANTED;ch.dirty=true}}}}}}
|
||||
|
||||
const STRUCTS=[
|
||||
{x:60,z:60,fn:crystalSpire},
|
||||
{x:-90,z:130,fn:ancientPortal},
|
||||
{x:220,z:-40,fn:floatingIsland},
|
||||
{x:-160,z:-120,fn:wizardTower},
|
||||
{x:140,z:-220,fn:crystalSpire},
|
||||
{x:-50,z:-180,fn:ancientPortal},
|
||||
{x:300,z:200,fn:wizardTower},
|
||||
{x:-250,z:50,fn:floatingIsland},
|
||||
{x:0,z:300,fn:ancientPortal},
|
||||
{x:200,z:150,fn:wizardTower},
|
||||
];
|
||||
|
||||
function applyStructures(cx,cz){
|
||||
for(const s of STRUCTS){
|
||||
const scx=Math.floor(s.x/CS),scz=Math.floor(s.z/CS);
|
||||
if(Math.abs(cx-scx)<=1&&Math.abs(cz-scz)<=1){
|
||||
s.fn(s.x,s.z)}}}
|
||||
|
||||
// ─── PLAYER ────────────────────────────────────────────────
|
||||
const P={pos:new THREE.Vector3(0,60,0),vel:new THREE.Vector3(),rot:{x:0,y:0},ground:false,eye:1.62,w:.6,h:1.75};
|
||||
let selSlot=0;
|
||||
const keys={};
|
||||
let locked=false;
|
||||
let lmb=false,rmb=false;
|
||||
let lastBreak=0,lastPlace=0;
|
||||
|
||||
const euler=new THREE.Euler(0,0,0,'YXZ');
|
||||
|
||||
document.addEventListener('keydown',e=>{keys[e.code]=true;if(e.code==='Escape')locked=false});
|
||||
document.addEventListener('keyup',e=>{keys[e.code]=false});
|
||||
document.addEventListener('mousemove',e=>{
|
||||
if(!locked)return;
|
||||
P.rot.y-=e.movementX*.002;
|
||||
P.rot.x-=e.movementY*.002;
|
||||
P.rot.x=Math.max(-Math.PI/2+.01,Math.min(Math.PI/2-.01,P.rot.x))});
|
||||
document.addEventListener('mousedown',e=>{
|
||||
if(!locked){renderer.domElement.requestPointerLock();return}
|
||||
if(e.button===0)lmb=true;
|
||||
if(e.button===2)rmb=true});
|
||||
document.addEventListener('mouseup',e=>{if(e.button===0)lmb=false;if(e.button===2)rmb=false});
|
||||
document.addEventListener('contextmenu',e=>e.preventDefault());
|
||||
document.addEventListener('wheel',e=>{
|
||||
selSlot=(selSlot+Math.sign(e.deltaY)+HB.length)%HB.length;
|
||||
updateHotbar()});
|
||||
document.addEventListener('keydown',e=>{
|
||||
const n=parseInt(e.key);
|
||||
if(n>=1&&n<=9){selSlot=n-1;updateHotbar()}});
|
||||
|
||||
document.addEventListener('pointerlockchange',()=>{
|
||||
locked=!!document.pointerLockElement;
|
||||
if(locked)document.getElementById('splash').style.display='none'});
|
||||
|
||||
function isSolid(b){return b!==B.AIR&&b!==B.WATER}
|
||||
function collide(px,py,pz){
|
||||
const hw=P.w/2;
|
||||
for(let bx=Math.floor(px-hw);bx<=Math.floor(px+hw);bx++)
|
||||
for(let by=Math.floor(py);by<=Math.floor(py+P.h);by++)
|
||||
for(let bz=Math.floor(pz-hw);bz<=Math.floor(pz+hw);bz++)
|
||||
if(isSolid(getBlock(bx,by,bz)))return true;
|
||||
return false}
|
||||
|
||||
function updatePlayer(dt){
|
||||
if(!locked)return;
|
||||
dt=Math.min(dt,.05);
|
||||
const sprint=keys['ShiftLeft']?1.6:1;
|
||||
const speed=6*sprint;
|
||||
const fwd=new THREE.Vector3(-Math.sin(P.rot.y),0,-Math.cos(P.rot.y));
|
||||
const rht=new THREE.Vector3(Math.cos(P.rot.y),0,-Math.sin(P.rot.y));
|
||||
const move=new THREE.Vector3();
|
||||
if(keys['KeyW'])move.add(fwd);
|
||||
if(keys['KeyS'])move.sub(fwd);
|
||||
if(keys['KeyD'])move.add(rht);
|
||||
if(keys['KeyA'])move.sub(rht);
|
||||
if(move.length()>.01)move.normalize().multiplyScalar(speed*dt);
|
||||
|
||||
const inWater=P.pos.y<WATER_L+1;
|
||||
const grav=inWater?-3:-20;
|
||||
P.vel.y+=grav*dt;
|
||||
if(inWater&&keys['Space'])P.vel.y=5;
|
||||
else if(P.ground&&keys['Space'])P.vel.y=8;
|
||||
P.vel.y=Math.max(P.vel.y,-40);
|
||||
|
||||
// X
|
||||
let nx=P.pos.x+move.x;
|
||||
if(!collide(nx,P.pos.y,P.pos.z))P.pos.x=nx;
|
||||
// Z
|
||||
let nz=P.pos.z+move.z;
|
||||
if(!collide(P.pos.x,P.pos.y,nz))P.pos.z=nz;
|
||||
// Y
|
||||
let ny=P.pos.y+P.vel.y*dt;
|
||||
if(!collide(P.pos.x,ny,P.pos.z)){P.pos.y=ny;P.ground=false}
|
||||
else{if(P.vel.y<0)P.ground=true;P.vel.y=0}
|
||||
|
||||
// prevent falling into void
|
||||
if(P.pos.y<-10){P.pos.y=80;P.vel.y=0}
|
||||
|
||||
camera.position.set(P.pos.x,P.pos.y+P.eye,P.pos.z);
|
||||
euler.set(P.rot.x,P.rot.y,0,'YXZ');
|
||||
camera.quaternion.setFromEuler(euler)}
|
||||
|
||||
// ─── RAYCAST ───────────────────────────────────────────────
|
||||
function raycast(maxD){
|
||||
const dir=new THREE.Vector3(0,0,-1).applyQuaternion(camera.quaternion);
|
||||
const o=camera.position.clone();
|
||||
let x=Math.floor(o.x),y=Math.floor(o.y),z=Math.floor(o.z);
|
||||
const sx=dir.x>0?1:-1,sy=dir.y>0?1:-1,sz=dir.z>0?1:-1;
|
||||
const tdx=Math.abs(1/dir.x),tdy=Math.abs(1/dir.y),tdz=Math.abs(1/dir.z);
|
||||
let tmx=dir.x>0?(x+1-o.x)*tdx:(o.x-x)*tdx;
|
||||
let tmy=dir.y>0?(y+1-o.y)*tdy:(o.y-y)*tdy;
|
||||
let tmz=dir.z>0?(z+1-o.z)*tdz:(o.z-z)*tdz;
|
||||
let px=x,py=y,pz=z;
|
||||
for(let i=0;i<maxD*3;i++){
|
||||
const b=getBlock(x,y,z);
|
||||
if(b!==B.AIR&&b!==B.WATER)return{x,y,z,b,px,py,pz};
|
||||
px=x;py=y;pz=z;
|
||||
if(tmx<tmy){if(tmx<tmz){x+=sx;tmx+=tdx}else{z+=sz;tmz+=tdz}}
|
||||
else{if(tmy<tmz){y+=sy;tmy+=tdy}else{z+=sz;tmz+=tdz}}}
|
||||
return null}
|
||||
|
||||
function doBreak(){
|
||||
if(Date.now()-lastBreak<200)return;
|
||||
lastBreak=Date.now();
|
||||
const hit=raycast(7);
|
||||
if(hit&&hit.b!==B.BEDROCK){setBlock(hit.x,hit.y,hit.z,B.AIR)}}
|
||||
|
||||
function doPlace(){
|
||||
if(Date.now()-lastPlace<200)return;
|
||||
lastPlace=Date.now();
|
||||
const hit=raycast(7);
|
||||
if(hit){
|
||||
const{px:bx,py:by,pz:bz}=hit;
|
||||
if(!isSolid(getBlock(bx,by,bz))){
|
||||
// Don't place where player stands
|
||||
const hw=P.w/2;
|
||||
const playerMinX=P.pos.x-hw,playerMaxX=P.pos.x+hw;
|
||||
const playerMinZ=P.pos.z-hw,playerMaxZ=P.pos.z+hw;
|
||||
const playerMinY=P.pos.y,playerMaxY=P.pos.y+P.h;
|
||||
if(bx+1>playerMinX&&bx<playerMaxX&&by+1>playerMinY&&by<playerMaxY&&bz+1>playerMinZ&&bz<playerMaxZ)return;
|
||||
setBlock(bx,by,bz,HB[selSlot])}}}
|
||||
|
||||
// ─── CHUNK MANAGEMENT ──────────────────────────────────────
|
||||
let lastCX=null,lastCZ=null;
|
||||
let meshBudget=3;
|
||||
|
||||
function updateChunks(){
|
||||
const pcx=Math.floor(P.pos.x/CS),pcz=Math.floor(P.pos.z/CS);
|
||||
if(pcx===lastCX&&pcz===lastCZ)return;
|
||||
lastCX=pcx;lastCZ=pcz;
|
||||
// Generate needed chunks
|
||||
for(let dx=-RD;dx<=RD;dx++)for(let dz=-RD;dz<=RD;dz++){
|
||||
if(dx*dx+dz*dz>RD*RD)continue;
|
||||
const cx=pcx+dx,cz=pcz+dz;
|
||||
const key=chunkKey(cx,cz);
|
||||
if(!chunks.has(key)){
|
||||
const ch=genChunk(cx,cz);
|
||||
applyStructures(cx,cz)}}
|
||||
// Remove far chunks
|
||||
for(const[key,ch] of chunks){
|
||||
const dx=ch.cx-pcx,dz=ch.cz-pcz;
|
||||
if(dx*dx+dz*dz>(RD+2)*(RD+2)){
|
||||
if(ch.mesh){scene.remove(ch.mesh);ch.mesh.geometry.dispose()}
|
||||
chunks.delete(key)}}}
|
||||
|
||||
function updateMeshes(){
|
||||
let built=0;
|
||||
for(const[key,ch] of chunks){
|
||||
if(built>=meshBudget)break;
|
||||
if(ch.dirty){
|
||||
if(ch.mesh){scene.remove(ch.mesh);ch.mesh.geometry.dispose()}
|
||||
const m=buildMesh(ch);
|
||||
if(m){scene.add(m);ch.mesh=m}
|
||||
ch.dirty=false;
|
||||
built++}}}
|
||||
|
||||
// ─── UI ────────────────────────────────────────────────────
|
||||
function updateHotbar(){
|
||||
const bar=document.getElementById('bar');
|
||||
bar.innerHTML='';
|
||||
HB.forEach((b,i)=>{
|
||||
const d=document.createElement('div');
|
||||
d.className='sl'+(i===selSlot?' a':'');
|
||||
d.innerHTML=`<span class="n">${i+1}</span>`;
|
||||
const tc=BTEX[b].t;
|
||||
const cv2=document.createElement('canvas');cv2.width=cv2.height=TS;
|
||||
const ctx2=cv2.getContext('2d');
|
||||
ctx2.drawImage(atlasCanvas,tc[0]*TS,tc[1]*TS,TS,TS,0,0,TS,TS);
|
||||
d.appendChild(cv2);
|
||||
bar.appendChild(d)});
|
||||
document.getElementById('bname').textContent=BN[HB[selSlot]]||''}
|
||||
|
||||
// ─── SUN ───────────────────────────────────────────────────
|
||||
function updateSun(){
|
||||
const t=performance.now()*.0001;
|
||||
sun.position.set(Math.cos(t)*200,200,Math.sin(t)*200)}
|
||||
|
||||
// ─── INIT & LOOP ──────────────────────────────────────────
|
||||
function init(){
|
||||
document.getElementById('splash').addEventListener('click',()=>{
|
||||
renderer.domElement.requestPointerLock()});
|
||||
// Pre-generate chunks around origin
|
||||
const pcx=0,pcz=0;
|
||||
for(let dx=-RD;dx<=RD;dx++)for(let dz=-RD;dz<=RD;dz++){
|
||||
if(dx*dx+dz*dz>RD*RD)continue;
|
||||
genChunk(pcx+dx,pcz+dz);
|
||||
applyStructures(pcx+dx,pcz+dz)}
|
||||
// Build initial meshes
|
||||
for(const[,ch] of chunks){if(ch.dirty){const m=buildMesh(ch);if(m){scene.add(m);ch.mesh=m}ch.dirty=false}}
|
||||
// Place player
|
||||
const startY=getTH(0,0)+3;
|
||||
P.pos.set(0,startY,0);
|
||||
updateHotbar();
|
||||
requestAnimationFrame(loop)}
|
||||
|
||||
let prevT=performance.now();
|
||||
let fc=0,fpsT=0,fps=0;
|
||||
|
||||
function loop(t){
|
||||
requestAnimationFrame(loop);
|
||||
const dt=(t-prevT)/1000;prevT=t;
|
||||
fc++;fpsT+=dt;if(fpsT>=1){fps=fc;fc=0;fpsT=0}
|
||||
updatePlayer(dt);
|
||||
updateChunks();
|
||||
meshBudget=3;
|
||||
updateMeshes();
|
||||
updateSun();
|
||||
// Interaction
|
||||
if(lmb)doBreak();
|
||||
if(rmb)doPlace();
|
||||
// Highlight
|
||||
const hit=raycast(7);
|
||||
if(hit){hlMesh.visible=true;hlMesh.position.set(hit.x+.5,hit.y+.5,hit.z+.5)}
|
||||
else{hlMesh.visible=false}
|
||||
// Debug
|
||||
const dbg=document.getElementById('dbg');
|
||||
dbg.innerHTML=`FPS: ${fps}<br>XYZ: ${P.pos.x.toFixed(1)} ${P.pos.y.toFixed(1)} ${P.pos.z.toFixed(1)}<br>Block: ${BN[HB[selSlot]]||'?'}`;
|
||||
renderer.render(scene,camera)}
|
||||
|
||||
window.addEventListener('resize',()=>{
|
||||
camera.aspect=innerWidth/innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(innerWidth,innerHeight)});
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,430 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 650" width="900" height="650">
|
||||
<defs>
|
||||
<!-- Sky gradient -->
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1a0533"/>
|
||||
<stop offset="25%" stop-color="#c0392b"/>
|
||||
<stop offset="50%" stop-color="#e67e22"/>
|
||||
<stop offset="75%" stop-color="#f4d03f"/>
|
||||
<stop offset="100%" stop-color="#fad7a0"/>
|
||||
</linearGradient>
|
||||
<!-- Ocean gradient -->
|
||||
<linearGradient id="ocean" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1a5276"/>
|
||||
<stop offset="40%" stop-color="#2471a3"/>
|
||||
<stop offset="100%" stop-color="#5dade2"/>
|
||||
</linearGradient>
|
||||
<!-- Sun reflection -->
|
||||
<radialGradient id="sunGlow" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0%" stop-color="#fff9c4" stop-opacity="1"/>
|
||||
<stop offset="40%" stop-color="#f9e542" stop-opacity="0.6"/>
|
||||
<stop offset="100%" stop-color="#e67e22" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<!-- Sand gradient -->
|
||||
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5cba7"/>
|
||||
<stop offset="100%" stop-color="#e8c39e"/>
|
||||
</linearGradient>
|
||||
<!-- Water reflection shimmer -->
|
||||
<linearGradient id="shimmer" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#fff" stop-opacity="0"/>
|
||||
<stop offset="50%" stop-color="#fff" stop-opacity="0.3"/>
|
||||
<stop offset="100%" stop-color="#fff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<!-- Pelican body gradient -->
|
||||
<linearGradient id="pelicanBody" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fdebd0"/>
|
||||
<stop offset="100%" stop-color="#f5cba7"/>
|
||||
</linearGradient>
|
||||
<!-- Pelican wing -->
|
||||
<linearGradient id="pelicanWing" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#d5d8dc"/>
|
||||
<stop offset="50%" stop-color="#aeb6bf"/>
|
||||
<stop offset="100%" stop-color="#85929e"/>
|
||||
</linearGradient>
|
||||
<!-- Pouch gradient -->
|
||||
<linearGradient id="pouch" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5b041"/>
|
||||
<stop offset="100%" stop-color="#e59866"/>
|
||||
</linearGradient>
|
||||
<!-- Cloud filter -->
|
||||
<filter id="softBlur">
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
</filter>
|
||||
<filter id="grain">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>
|
||||
<feColorMatrix type="saturate" values="0"/>
|
||||
<feBlend in="SourceGraphic" mode="multiply" result="blend"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.08"/>
|
||||
</feComponentTransfer>
|
||||
<feBlend in="SourceGraphic" mode="normal"/>
|
||||
</filter>
|
||||
<!-- Shadow -->
|
||||
<filter id="shadow">
|
||||
<feDropShadow dx="2" dy="4" stdDeviation="3" flood-color="#000" flood-opacity="0.25"/>
|
||||
</filter>
|
||||
<!-- Water sparkle -->
|
||||
<radialGradient id="sparkle" cx="0.5" cy="0.5" r="0.5">
|
||||
<stop offset="0%" stop-color="#fff" stop-opacity="0.9"/>
|
||||
<stop offset="100%" stop-color="#fff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<!-- SKY -->
|
||||
<rect width="900" height="650" fill="url(#sky)"/>
|
||||
|
||||
<!-- SUN -->
|
||||
<ellipse cx="680" cy="260" rx="90" ry="90" fill="url(#sunGlow)"/>
|
||||
<circle cx="680" cy="260" r="38" fill="#fff3b0" opacity="0.95"/>
|
||||
<circle cx="680" cy="260" r="28" fill="#fffde7"/>
|
||||
|
||||
<!-- CLOUDS -->
|
||||
<g opacity="0.6" filter="url(#softBlur)">
|
||||
<ellipse cx="150" cy="80" rx="80" ry="25" fill="#fdebd0" opacity="0.5"/>
|
||||
<ellipse cx="190" cy="70" rx="60" ry="20" fill="#fdebd0" opacity="0.4"/>
|
||||
<ellipse cx="400" cy="110" rx="100" ry="20" fill="#fad7a0" opacity="0.35"/>
|
||||
<ellipse cx="450" cy="100" rx="70" ry="18" fill="#fad7a0" opacity="0.3"/>
|
||||
<ellipse cx="600" cy="60" rx="50" ry="15" fill="#fdebd0" opacity="0.3"/>
|
||||
<ellipse cx="800" cy="130" rx="90" ry="22" fill="#fad7a0" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<!-- BIRDS IN DISTANCE -->
|
||||
<g fill="none" stroke="#1a0533" stroke-width="1.5" stroke-linecap="round" opacity="0.3">
|
||||
<path d="M100,150 Q108,142 116,150 Q124,142 132,150"/>
|
||||
<path d="M160,130 Q166,124 172,130 Q178,124 184,130"/>
|
||||
<path d="M130,170 Q135,164 140,170 Q145,164 150,170"/>
|
||||
</g>
|
||||
|
||||
<!-- OCEAN -->
|
||||
<path d="M0,340 Q100,330 200,338 Q350,325 450,335 Q550,328 700,340 Q800,332 900,338 L900,430 L0,430 Z" fill="url(#ocean)"/>
|
||||
|
||||
<!-- OCEAN WAVES -->
|
||||
<path d="M0,360 Q60,355 120,362 Q200,352 300,360 Q400,350 500,358 Q600,348 700,360 Q800,352 900,358" fill="none" stroke="#1a5276" stroke-width="1" opacity="0.4"/>
|
||||
<path d="M0,380 Q80,374 160,382 Q260,372 360,380 Q480,370 580,378 Q700,368 800,380 Q860,374 900,378" fill="none" stroke="#1a5276" stroke-width="0.8" opacity="0.3"/>
|
||||
<path d="M0,400 Q100,395 200,402 Q320,393 420,400 Q540,391 640,399 Q750,390 900,398" fill="none" stroke="#2471a3" stroke-width="0.6" opacity="0.25"/>
|
||||
|
||||
<!-- Water sparkles -->
|
||||
<g>
|
||||
<ellipse cx="620" cy="350" rx="8" ry="2" fill="url(#sparkle)" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;0.1;0.5" dur="3s" repeatCount="indefinite"/>
|
||||
</ellipse>
|
||||
<ellipse cx="500" cy="370" rx="6" ry="1.5" fill="url(#sparkle)" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="2.5s" repeatCount="indefinite"/>
|
||||
</ellipse>
|
||||
<ellipse cx="720" cy="365" rx="5" ry="1.5" fill="url(#sparkle)" opacity="0.6">
|
||||
<animate attributeName="opacity" values="0.6;0.15;0.6" dur="3.5s" repeatCount="indefinite"/>
|
||||
</ellipse>
|
||||
<ellipse cx="350" cy="385" rx="7" ry="2" fill="url(#sparkle)" opacity="0.3">
|
||||
<animate attributeName="opacity" values="0.3;0.05;0.3" dur="2s" repeatCount="indefinite"/>
|
||||
</ellipse>
|
||||
</g>
|
||||
|
||||
<!-- Sun reflection on water -->
|
||||
<g opacity="0.3">
|
||||
<rect x="650" y="340" width="60" height="90" rx="30" fill="#f9e542" opacity="0.15"/>
|
||||
<rect x="665" y="345" width="30" height="80" rx="15" fill="#fff9c4" opacity="0.2"/>
|
||||
</g>
|
||||
|
||||
<!-- BEACH / SAND -->
|
||||
<path d="M0,420 Q150,410 300,418 Q500,408 700,420 Q850,415 900,422 L900,650 L0,650 Z" fill="url(#sand)"/>
|
||||
|
||||
<!-- Wet sand near water -->
|
||||
<path d="M0,430 Q150,422 300,428 Q500,418 700,430 Q850,425 900,432 L900,460 Q850,450 700,455 Q500,445 300,452 Q150,446 0,455 Z" fill="#d7ccc8" opacity="0.4"/>
|
||||
|
||||
<!-- Foam line -->
|
||||
<path d="M0,428 Q60,423 130,427 Q200,421 280,426 Q380,420 480,425 Q560,419 650,426 Q750,420 840,427 Q880,423 900,426" fill="none" stroke="white" stroke-width="2" opacity="0.5">
|
||||
<animate attributeName="opacity" values="0.5;0.2;0.5" dur="4s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<path d="M0,432 Q80,427 160,431 Q260,425 350,430 Q460,424 560,429 Q680,423 780,430 Q860,425 900,429" fill="none" stroke="white" stroke-width="1.2" opacity="0.35">
|
||||
<animate attributeName="opacity" values="0.35;0.1;0.35" dur="3.5s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
|
||||
<!-- Sand texture dots -->
|
||||
<g fill="#c9a96e" opacity="0.25">
|
||||
<circle cx="50" cy="500" r="1"/><circle cx="120" cy="520" r="0.8"/><circle cx="200" cy="490" r="1.2"/>
|
||||
<circle cx="300" cy="510" r="0.7"/><circle cx="400" cy="505" r="1"/><circle cx="500" cy="530" r="0.9"/>
|
||||
<circle cx="600" cy="495" r="1.1"/><circle cx="700" cy="515" r="0.8"/><circle cx="800" cy="500" r="1"/>
|
||||
<circle cx="150" cy="550" r="0.6"/><circle cx="350" cy="540" r="1"/><circle cx="550" cy="560" r="0.7"/>
|
||||
<circle cx="750" cy="545" r="0.9"/><circle cx="250" cy="570" r="1.1"/><circle cx="650" cy="575" r="0.8"/>
|
||||
</g>
|
||||
|
||||
<!-- SHELLS on beach -->
|
||||
<g transform="translate(100,470)" opacity="0.5">
|
||||
<ellipse cx="0" cy="0" rx="6" ry="4" fill="#f5e6cc" stroke="#d5c4a1" stroke-width="0.5"/>
|
||||
<line x1="-4" y1="0" x2="4" y2="0" stroke="#d5c4a1" stroke-width="0.3"/>
|
||||
</g>
|
||||
<g transform="translate(760,480) rotate(30)" opacity="0.4">
|
||||
<ellipse cx="0" cy="0" rx="5" ry="3" fill="#fce4d6" stroke="#d5c4a1" stroke-width="0.5"/>
|
||||
</g>
|
||||
<g transform="translate(420,520) rotate(-15)" opacity="0.35">
|
||||
<path d="M0,0 Q-4,-5 0,-8 Q4,-5 0,0" fill="#f0d9c4" stroke="#c9a96e" stroke-width="0.4"/>
|
||||
</g>
|
||||
|
||||
<!-- SEAGRASS -->
|
||||
<g stroke="#6b8e23" stroke-width="1.5" fill="none" opacity="0.5" stroke-linecap="round">
|
||||
<path d="M30,440 Q25,420 35,400 Q30,395 28,385"/>
|
||||
<path d="M35,440 Q40,418 32,400 Q38,390 33,378"/>
|
||||
<path d="M820,445 Q825,425 818,405 Q823,395 820,385"/>
|
||||
<path d="M825,445 Q818,428 826,408 Q820,398 825,388"/>
|
||||
</g>
|
||||
|
||||
<!-- ========== BICYCLE ========== -->
|
||||
<g transform="translate(350, 0)" filter="url(#shadow)">
|
||||
|
||||
<!-- Bike shadow on sand -->
|
||||
<ellipse cx="80" cy="540" rx="100" ry="12" fill="#000" opacity="0.12"/>
|
||||
|
||||
<!-- Rear wheel -->
|
||||
<g transform="translate(0, 440)">
|
||||
<circle cx="0" cy="0" r="65" fill="none" stroke="#2c3e50" stroke-width="4"/>
|
||||
<circle cx="0" cy="0" r="60" fill="none" stroke="#34495e" stroke-width="1.5" opacity="0.4"/>
|
||||
<!-- Tire tread -->
|
||||
<circle cx="0" cy="0" r="67" fill="none" stroke="#1a252f" stroke-width="2" stroke-dasharray="4,4" opacity="0.3"/>
|
||||
<!-- Hub -->
|
||||
<circle cx="0" cy="0" r="6" fill="#7f8c8d"/>
|
||||
<circle cx="0" cy="0" r="3" fill="#bdc3c7"/>
|
||||
<!-- Spokes -->
|
||||
<g stroke="#95a5a6" stroke-width="0.8" opacity="0.6">
|
||||
<line x1="0" y1="-6" x2="0" y2="-60"/><line x1="0" y1="6" x2="0" y2="60"/>
|
||||
<line x1="-6" y1="0" x2="-60" y2="0"/><line x1="6" y1="0" x2="60" y2="0"/>
|
||||
<line x1="-4" y1="-4" x2="-42" y2="-42"/><line x1="4" y1="4" x2="42" y2="42"/>
|
||||
<line x1="-4" y1="4" x2="-42" y2="42"/><line x1="4" y1="-4" x2="42" y2="-42"/>
|
||||
<line x1="-2" y1="-5" x2="-20" y2="-58"/><line x1="2" y1="5" x2="20" y2="58"/>
|
||||
<line x1="-5" y1="-2" x2="-58" y2="-20"/><line x1="5" y1="2" x2="58" y2="20"/>
|
||||
<line x1="-5" y1="2" x2="-58" y2="20"/><line x1="5" y1="-2" x2="58" y2="-20"/>
|
||||
<line x1="-2" y1="5" x2="-20" y2="58"/><line x1="2" y1="-5" x2="20" y2="-58"/>
|
||||
</g>
|
||||
<!-- Wheel rim highlight -->
|
||||
<circle cx="0" cy="0" r="62" fill="none" stroke="#ecf0f1" stroke-width="0.5" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<!-- Front wheel -->
|
||||
<g transform="translate(180, 440)">
|
||||
<circle cx="0" cy="0" r="65" fill="none" stroke="#2c3e50" stroke-width="4"/>
|
||||
<circle cx="0" cy="0" r="60" fill="none" stroke="#34495e" stroke-width="1.5" opacity="0.4"/>
|
||||
<circle cx="0" cy="0" r="67" fill="none" stroke="#1a252f" stroke-width="2" stroke-dasharray="4,4" opacity="0.3"/>
|
||||
<circle cx="0" cy="0" r="6" fill="#7f8c8d"/>
|
||||
<circle cx="0" cy="0" r="3" fill="#bdc3c7"/>
|
||||
<g stroke="#95a5a6" stroke-width="0.8" opacity="0.6">
|
||||
<line x1="0" y1="-6" x2="0" y2="-60"/><line x1="0" y1="6" x2="0" y2="60"/>
|
||||
<line x1="-6" y1="0" x2="-60" y2="0"/><line x1="6" y1="0" x2="60" y2="0"/>
|
||||
<line x1="-4" y1="-4" x2="-42" y2="-42"/><line x1="4" y1="4" x2="42" y2="42"/>
|
||||
<line x1="-4" y1="4" x2="-42" y2="42"/><line x1="4" y1="-4" x2="42" y2="-42"/>
|
||||
<line x1="-2" y1="-5" x2="-20" y2="-58"/><line x1="2" y1="5" x2="20" y2="58"/>
|
||||
<line x1="-5" y1="-2" x2="-58" y2="-20"/><line x1="5" y1="2" x2="58" y2="20"/>
|
||||
<line x1="-5" y1="2" x2="-58" y2="20"/><line x1="5" y1="-2" x2="58" y2="-20"/>
|
||||
<line x1="-2" y1="5" x2="-20" y2="58"/><line x1="2" y1="-5" x2="20" y2="-58"/>
|
||||
</g>
|
||||
<circle cx="0" cy="0" r="62" fill="none" stroke="#ecf0f1" stroke-width="0.5" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<!-- Chain ring -->
|
||||
<circle cx="70" cy="440" r="18" fill="none" stroke="#5d6d7e" stroke-width="2.5"/>
|
||||
<circle cx="70" cy="440" r="14" fill="none" stroke="#85929e" stroke-width="1" opacity="0.5"/>
|
||||
<circle cx="70" cy="440" r="4" fill="#5d6d7e"/>
|
||||
<!-- Chain -->
|
||||
<path d="M70,458 L0,446" stroke="#2c3e50" stroke-width="2.5" fill="none"/>
|
||||
<path d="M70,422 L0,434" stroke="#2c3e50" stroke-width="2.5" fill="none"/>
|
||||
<!-- Crank arm -->
|
||||
<line x1="70" y1="440" x2="85" y2="465" stroke="#5d6d7e" stroke-width="4" stroke-linecap="round"/>
|
||||
<!-- Pedal -->
|
||||
<rect x="78" y="462" width="18" height="5" rx="2" fill="#2c3e50" transform="rotate(10, 87, 465)"/>
|
||||
|
||||
<!-- Frame -->
|
||||
<g stroke="#8e1a1a" stroke-width="4.5" stroke-linecap="round" fill="none">
|
||||
<!-- Seat tube -->
|
||||
<line x1="70" y1="440" x2="80" y2="350"/>
|
||||
<!-- Top tube -->
|
||||
<line x1="80" y1="350" x2="170" y2="360"/>
|
||||
<!-- Down tube -->
|
||||
<line x1="70" y1="440" x2="170" y2="380"/>
|
||||
<!-- Seat stay -->
|
||||
<line x1="80" y1="350" x2="0" y2="440"/>
|
||||
<!-- Chain stay -->
|
||||
<line x1="70" y1="440" x2="0" y2="440"/>
|
||||
<!-- Head tube -->
|
||||
<line x1="170" y1="360" x2="175" y2="380"/>
|
||||
<!-- Fork -->
|
||||
<line x1="172" y1="375" x2="180" y2="440"/>
|
||||
</g>
|
||||
|
||||
<!-- Frame accent stripe -->
|
||||
<g stroke="#c0392b" stroke-width="1.5" stroke-linecap="round" fill="none" opacity="0.5">
|
||||
<line x1="72" y1="435" x2="82" y2="355"/>
|
||||
<line x1="82" y1="352" x2="168" y2="362"/>
|
||||
</g>
|
||||
|
||||
<!-- Handlebar -->
|
||||
<path d="M168,355 Q170,340 178,335 Q185,332 192,338" stroke="#2c3e50" stroke-width="3.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Handlebar grips -->
|
||||
<line x1="190" y1="336" x2="195" y2="332" stroke="#1a1a1a" stroke-width="5" stroke-linecap="round"/>
|
||||
|
||||
<!-- Seat post -->
|
||||
<line x1="78" y1="352" x2="75" y2="332" stroke="#5d6d7e" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- Seat -->
|
||||
<ellipse cx="75" cy="330" rx="18" ry="6" fill="#1a1a1a"/>
|
||||
<ellipse cx="75" cy="329" rx="16" ry="4.5" fill="#2c2c2c"/>
|
||||
<ellipse cx="72" cy="328" rx="8" ry="2" fill="#3d3d3d" opacity="0.5"/>
|
||||
</g>
|
||||
|
||||
<!-- ========== PELICAN ========== -->
|
||||
<g transform="translate(350, 0)" filter="url(#shadow)">
|
||||
|
||||
<!-- TAIL FEATHERS -->
|
||||
<g transform="translate(50, 300)">
|
||||
<path d="M0,0 Q-30,10 -50,30 Q-40,20 -20,15 Q-45,25 -60,45 Q-45,30 -18,22 Q-40,35 -55,55 Q-38,35 -12,28 Q-30,40 -48,62 Q-30,40 -8,32 Q-20,45 -35,65 Q-18,45 -5,35" fill="url(#pelicanWing)" stroke="#85929e" stroke-width="0.5"/>
|
||||
<path d="M0,0 Q-25,15 -45,35" stroke="#aeb6bf" stroke-width="0.5" fill="none" opacity="0.5"/>
|
||||
</g>
|
||||
|
||||
<!-- BODY -->
|
||||
<ellipse cx="90" cy="320" rx="55" ry="38" fill="url(#pelicanBody)" transform="rotate(-5, 90, 320)"/>
|
||||
<!-- Body shading -->
|
||||
<ellipse cx="95" cy="330" rx="40" ry="25" fill="#fdebd0" opacity="0.3"/>
|
||||
<!-- Breast feathers -->
|
||||
<g fill="none" stroke="#e8c39e" stroke-width="0.6" opacity="0.5">
|
||||
<path d="M70,310 Q80,315 70,320"/>
|
||||
<path d="M80,308 Q90,313 80,318"/>
|
||||
<path d="M90,307 Q100,312 90,317"/>
|
||||
<path d="M100,308 Q110,313 100,318"/>
|
||||
<path d="M75,320 Q85,325 75,330"/>
|
||||
<path d="M85,318 Q95,323 85,328"/>
|
||||
<path d="M95,317 Q105,322 95,327"/>
|
||||
</g>
|
||||
|
||||
<!-- WING (visible part) -->
|
||||
<path d="M60,300 Q40,310 35,330 Q38,350 55,365 Q75,375 100,370 Q120,360 130,345 Q135,330 130,315 Q120,300 100,295 Z" fill="url(#pelicanWing)" stroke="#85929e" stroke-width="0.8"/>
|
||||
<!-- Wing feather detail -->
|
||||
<g fill="none" stroke="#85929e" stroke-width="0.5" opacity="0.6">
|
||||
<path d="M55,330 Q70,340 90,345"/>
|
||||
<path d="M50,340 Q65,350 85,355"/>
|
||||
<path d="M48,350 Q62,358 80,363"/>
|
||||
<path d="M55,358 Q70,365 88,368"/>
|
||||
<path d="M65,362 Q80,368 100,370"/>
|
||||
</g>
|
||||
<!-- Wing tip highlight -->
|
||||
<path d="M38,335 Q35,342 40,350" stroke="#d5d8dc" stroke-width="1" fill="none" opacity="0.4"/>
|
||||
|
||||
<!-- NECK -->
|
||||
<path d="M120,305 Q125,280 130,260 Q138,235 150,218 Q158,208 165,200" stroke="#f5cba7" stroke-width="22" fill="none" stroke-linecap="round"/>
|
||||
<!-- Neck feather overlay for white stripe -->
|
||||
<path d="M125,295 Q130,270 137,250 Q144,230 155,215" stroke="#fdebd0" stroke-width="12" fill="none" stroke-linecap="round" opacity="0.6"/>
|
||||
<!-- Neck shading -->
|
||||
<path d="M118,300 Q122,278 128,258 Q135,238 148,220" stroke="#e8c39e" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.3"/>
|
||||
|
||||
<!-- HEAD -->
|
||||
<ellipse cx="170" cy="190" rx="22" ry="18" fill="#fdebd0" transform="rotate(-10, 170, 190)"/>
|
||||
<!-- Crown feathers -->
|
||||
<path d="M155,178 Q160,170 170,175 Q175,168 183,174 Q188,170 192,178" fill="#f5cba7" stroke="#e8c39e" stroke-width="0.5"/>
|
||||
<!-- Eye area shading -->
|
||||
<ellipse cx="175" cy="185" rx="10" ry="8" fill="#fce4d6" opacity="0.5"/>
|
||||
<!-- Eye ring -->
|
||||
<circle cx="178" cy="184" r="5.5" fill="#fff"/>
|
||||
<circle cx="178" cy="184" r="4" fill="#1a0533"/>
|
||||
<circle cx="178" cy="183.5" r="3" fill="#2c1654"/>
|
||||
<circle cx="180" cy="182" r="1.2" fill="#fff"/>
|
||||
<!-- Eye ring color -->
|
||||
<circle cx="178" cy="184" r="5.5" fill="none" stroke="#f0b27a" stroke-width="1.2"/>
|
||||
<!-- Brow line -->
|
||||
<path d="M172,179 Q178,176 185,178" stroke="#d5c4a1" stroke-width="0.8" fill="none"/>
|
||||
|
||||
<!-- BEAK UPPER -->
|
||||
<path d="M190,188 Q210,185 255,192 Q260,193 258,195 Q250,196 240,196 Q220,198 200,196 L190,194 Z" fill="#f0a030" stroke="#d4890a" stroke-width="0.8"/>
|
||||
<!-- Beak ridge -->
|
||||
<path d="M195,190 Q220,187 250,193" stroke="#e09020" stroke-width="1.2" fill="none"/>
|
||||
<!-- Beak tip hook -->
|
||||
<path d="M255,192 Q262,193 260,196 Q258,197 255,196" fill="#d4890a"/>
|
||||
<!-- Nostril -->
|
||||
<ellipse cx="210" cy="191" rx="3" ry="1.5" fill="#d4890a" opacity="0.5"/>
|
||||
|
||||
<!-- BEAK LOWER / POUCH -->
|
||||
<path d="M190,194 Q195,198 200,210 Q210,225 225,228 Q240,225 248,215 Q255,205 250,196 Q240,198 220,198 Q200,198 190,194 Z" fill="url(#pouch)" stroke="#d4890a" stroke-width="0.6" opacity="0.9"/>
|
||||
<!-- Pouch texture lines -->
|
||||
<g fill="none" stroke="#d4890a" stroke-width="0.4" opacity="0.4">
|
||||
<path d="M198,200 Q210,215 225,220"/>
|
||||
<path d="M205,200 Q215,212 230,218"/>
|
||||
<path d="M215,199 Q225,210 238,212"/>
|
||||
<path d="M240,205 Q245,210 248,208"/>
|
||||
</g>
|
||||
<!-- Pouch highlight -->
|
||||
<path d="M200,200 Q215,215 230,220" stroke="#f5cba7" stroke-width="0.8" fill="none" opacity="0.3"/>
|
||||
|
||||
<!-- LEGS ON PEDALS -->
|
||||
<!-- Right leg (on pedal) -->
|
||||
<g>
|
||||
<!-- Upper leg/thigh -->
|
||||
<path d="M85,345 Q82,370 83,395 Q84,410 90,425" stroke="#f0a030" stroke-width="7" fill="none" stroke-linecap="round"/>
|
||||
<!-- Lower leg -->
|
||||
<path d="M90,425 Q92,440 95,455" stroke="#f0a030" stroke-width="5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Foot webbed -->
|
||||
<g transform="translate(95, 458)">
|
||||
<ellipse cx="0" cy="0" rx="12" ry="4" fill="#f0a030" stroke="#d4890a" stroke-width="0.5"/>
|
||||
<line x1="-8" y1="0" x2="-15" y2="5" stroke="#d4890a" stroke-width="1.5"/>
|
||||
<line x1="0" y1="0" x2="0" y2="8" stroke="#d4890a" stroke-width="1.5"/>
|
||||
<line x1="8" y1="0" x2="15" y2="5" stroke="#d4890a" stroke-width="1.5"/>
|
||||
<!-- Webbing -->
|
||||
<path d="M-8,0 Q-10,3 -15,5" fill="#f5b041" opacity="0.5"/>
|
||||
<path d="M0,0 Q0,5 0,8" fill="#f5b041" opacity="0.5"/>
|
||||
<path d="M8,0 Q10,3 15,5" fill="#f5b041" opacity="0.5"/>
|
||||
</g>
|
||||
</g>
|
||||
<!-- Left leg (slightly behind) -->
|
||||
<g opacity="0.85">
|
||||
<path d="M75,348 Q70,375 72,400 Q73,418 78,430" stroke="#e09020" stroke-width="6" fill="none" stroke-linecap="round"/>
|
||||
<path d="M78,430 Q80,445 82,455" stroke="#e09020" stroke-width="4.5" fill="none" stroke-linecap="round"/>
|
||||
<g transform="translate(82, 458)">
|
||||
<ellipse cx="0" cy="0" rx="11" ry="3.5" fill="#e09020" stroke="#c4820a" stroke-width="0.5"/>
|
||||
<line x1="-7" y1="0" x2="-13" y2="4" stroke="#c4820a" stroke-width="1.2"/>
|
||||
<line x1="0" y1="0" x2="0" y2="7" stroke="#c4820a" stroke-width="1.2"/>
|
||||
<line x1="7" y1="0" x2="13" y2="4" stroke="#c4820a" stroke-width="1.2"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- WINGS HOLDING HANDLEBARS -->
|
||||
<!-- Right wing/arm extended to handlebar -->
|
||||
<path d="M110,310 Q130,305 150,298 Q165,294 180,296 Q195,300 205,310" stroke="#aeb6bf" stroke-width="8" fill="none" stroke-linecap="round"/>
|
||||
<path d="M110,310 Q130,305 150,298 Q165,294 180,296 Q195,300 205,310" stroke="#c0c8d0" stroke-width="5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Wing tip gripping handlebar -->
|
||||
<path d="M205,310 Q208,308 210,305 Q212,308 210,312 Q208,310 205,310" fill="#aeb6bf"/>
|
||||
|
||||
<!-- Left wing/arm (partially visible) -->
|
||||
<path d="M100,315 Q115,312 135,308 Q155,305 175,305 Q190,307 200,312" stroke="#95a5a6" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.7"/>
|
||||
</g>
|
||||
|
||||
<!-- Sand spray behind wheels -->
|
||||
<g opacity="0.35">
|
||||
<path d="M425,530 Q415,520 410,505 Q408,498 412,490" fill="none" stroke="#d5c4a1" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M435,532 Q428,525 425,515 Q424,508 427,500" fill="none" stroke="#d5c4a1" stroke-width="1" stroke-linecap="round"/>
|
||||
<path d="M418,528 Q408,518 402,502 Q400,495 405,488" fill="none" stroke="#c9a96e" stroke-width="1" stroke-linecap="round"/>
|
||||
<!-- Sand particles -->
|
||||
<circle cx="405" cy="485" r="1.5" fill="#d5c4a1"/>
|
||||
<circle cx="410" cy="490" r="1" fill="#c9a96e"/>
|
||||
<circle cx="400" cy="495" r="1.2" fill="#d5c4a1"/>
|
||||
<circle cx="415" cy="488" r="0.8" fill="#e8c39e"/>
|
||||
<circle cx="408" cy="500" r="1" fill="#d5c4a1"/>
|
||||
</g>
|
||||
|
||||
<!-- Pelican shadow -->
|
||||
<ellipse cx="430" cy="540" rx="70" ry="8" fill="#000" opacity="0.1"/>
|
||||
|
||||
<!-- Beach grass in foreground -->
|
||||
<g stroke="#6b8e23" stroke-width="1.8" fill="none" opacity="0.6" stroke-linecap="round">
|
||||
<path d="M20,580 Q15,560 25,540 Q18,530 22,518"/>
|
||||
<path d="M26,580 Q30,558 22,538 Q28,528 24,515"/>
|
||||
<path d="M32,580 Q28,562 34,545"/>
|
||||
<path d="M860,570 Q855,552 862,535 Q856,525 860,512"/>
|
||||
<path d="M866,570 Q870,550 864,532 Q870,522 865,510"/>
|
||||
<path d="M872,570 Q868,555 874,540"/>
|
||||
</g>
|
||||
|
||||
<!-- Distant sailboat -->
|
||||
<g transform="translate(250, 310)" opacity="0.35">
|
||||
<path d="M0,0 L3,-25 L6,0 Z" fill="#fff"/>
|
||||
<path d="M3,-25 L3,-5 L10,-5 Z" fill="#ecf0f1"/>
|
||||
<line x1="3" y1="0" x2="3" y2="-25" stroke="#7f8c8d" stroke-width="0.5"/>
|
||||
<path d="M-3,2 Q3,0 9,2 Q6,3 3,2 Q0,3 -3,2 Z" fill="#5dade2"/>
|
||||
</g>
|
||||
|
||||
<!-- Overall grain overlay -->
|
||||
<rect width="900" height="650" fill="url(#grain)" opacity="0.03"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,291 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1565C0"/>
|
||||
<stop offset="40%" stop-color="#42A5F5"/>
|
||||
<stop offset="100%" stop-color="#B3E5FC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ocean" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#0277BD"/>
|
||||
<stop offset="100%" stop-color="#4FC3F7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sand" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#F9E4B7"/>
|
||||
<stop offset="100%" stop-color="#DEB887"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="sunGlow" cx="50%" cy="50%">
|
||||
<stop offset="0%" stop-color="#FFF9C4"/>
|
||||
<stop offset="30%" stop-color="#FFD54F" stop-opacity="0.6"/>
|
||||
<stop offset="100%" stop-color="#FF8F00" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="bodyGrad" cx="40%" cy="40%">
|
||||
<stop offset="0%" stop-color="#FFFFF5"/>
|
||||
<stop offset="100%" stop-color="#F0EBD8"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="pouchG" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#FF9800"/>
|
||||
<stop offset="100%" stop-color="#E65100"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="wetSand" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#C4A265"/>
|
||||
<stop offset="50%" stop-color="#D4B87A"/>
|
||||
<stop offset="100%" stop-color="#C4A265"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Sky -->
|
||||
<rect width="800" height="600" fill="url(#sky)"/>
|
||||
|
||||
<!-- Sun with glow -->
|
||||
<circle cx="650" cy="110" r="100" fill="url(#sunGlow)"/>
|
||||
<circle cx="650" cy="110" r="38" fill="#FDD835"/>
|
||||
<circle cx="650" cy="110" r="32" fill="#FFEE58"/>
|
||||
|
||||
<!-- Clouds -->
|
||||
<g opacity="0.92">
|
||||
<ellipse cx="120" cy="85" rx="55" ry="22" fill="#fff"/>
|
||||
<ellipse cx="160" cy="72" rx="48" ry="28" fill="#fff"/>
|
||||
<ellipse cx="200" cy="80" rx="42" ry="20" fill="#fff"/>
|
||||
<ellipse cx="145" cy="90" rx="30" ry="12" fill="#f0f8ff"/>
|
||||
</g>
|
||||
<g opacity="0.7">
|
||||
<ellipse cx="450" cy="55" rx="40" ry="16" fill="#fff"/>
|
||||
<ellipse cx="480" cy="45" rx="35" ry="22" fill="#fff"/>
|
||||
<ellipse cx="510" cy="55" rx="30" ry="14" fill="#fff"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<ellipse cx="320" cy="110" rx="30" ry="12" fill="#fff"/>
|
||||
<ellipse cx="340" cy="103" rx="25" ry="16" fill="#fff"/>
|
||||
</g>
|
||||
|
||||
<!-- Distant birds -->
|
||||
<g fill="none" stroke="#455A64" stroke-width="1.5" stroke-linecap="round" opacity="0.35">
|
||||
<path d="M580,130 Q585,124 590,130 Q595,124 600,130"/>
|
||||
<path d="M610,145 Q614,140 618,145 Q622,140 626,145"/>
|
||||
<path d="M555,155 Q558,151 561,155 Q564,151 567,155"/>
|
||||
</g>
|
||||
|
||||
<!-- Ocean base -->
|
||||
<rect x="0" y="260" width="800" height="160" fill="url(#ocean)"/>
|
||||
|
||||
<!-- Wave layers -->
|
||||
<path d="M0,280 Q40,270 80,280 Q120,290 160,280 Q200,270 240,280 Q280,290 320,280 Q360,270 400,280 Q440,290 480,280 Q520,270 560,280 Q600,290 640,280 Q680,270 720,280 Q760,290 800,280 V300 Q760,295 720,300 Q680,305 640,300 Q600,295 560,300 Q520,305 480,300 Q440,295 400,300 Q360,305 320,300 Q280,295 240,300 Q200,305 160,300 Q120,295 80,300 Q40,305 0,300 Z" fill="#29B6F6" opacity="0.5"/>
|
||||
<path d="M0,310 Q50,300 100,310 Q150,320 200,310 Q250,300 300,310 Q350,320 400,310 Q450,300 500,310 Q550,320 600,310 Q650,300 700,310 Q750,320 800,310 V330 Q750,326 700,330 Q650,334 600,330 Q550,326 500,330 Q450,334 400,330 Q350,326 300,330 Q250,334 200,330 Q150,326 100,330 Q50,334 0,330 Z" fill="#4FC3F7" opacity="0.4"/>
|
||||
<path d="M0,345 Q60,338 120,345 Q180,352 240,345 Q300,338 360,345 Q420,352 480,345 Q540,338 600,345 Q660,352 720,345 Q780,338 800,345" fill="none" stroke="#B3E5FC" stroke-width="2" opacity="0.6"/>
|
||||
|
||||
<!-- Foam line at shore -->
|
||||
<path d="M0,395 Q30,388 60,395 Q90,402 120,395 Q150,388 180,395 Q210,402 240,395 Q270,388 300,395 Q330,402 360,395 Q390,388 420,395 Q450,402 480,395 Q510,388 540,395 Q570,402 600,395 Q630,388 660,395 Q690,402 720,395 Q750,388 780,395 Q800,400 800,395" fill="none" stroke="white" stroke-width="3" opacity="0.85"/>
|
||||
<path d="M0,398 Q35,392 70,398 Q105,404 140,398 Q175,392 210,398 Q245,404 280,398 Q315,392 350,398 Q385,404 420,398 Q455,392 490,398 Q525,404 560,398 Q595,392 630,398 Q665,404 700,398 Q735,392 770,398 L800,398" fill="none" stroke="white" stroke-width="1.5" opacity="0.5"/>
|
||||
|
||||
<!-- Beach -->
|
||||
<path d="M0,405 Q200,388 400,400 Q600,412 800,395 L800,600 L0,600 Z" fill="url(#sand)"/>
|
||||
|
||||
<!-- Wet sand area -->
|
||||
<path d="M0,405 Q200,388 400,400 Q600,412 800,395 L800,435 Q600,445 400,430 Q200,415 0,440 Z" fill="url(#wetSand)" opacity="0.6"/>
|
||||
|
||||
<!-- Sand texture dots -->
|
||||
<g fill="#C8A96E" opacity="0.3">
|
||||
<circle cx="80" cy="470" r="1.5"/><circle cx="180" cy="500" r="1"/><circle cx="350" cy="480" r="1.5"/>
|
||||
<circle cx="500" cy="460" r="1"/><circle cx="620" cy="490" r="1.5"/><circle cx="710" cy="475" r="1"/>
|
||||
<circle cx="150" cy="530" r="1"/><circle cx="280" cy="550" r="1.5"/><circle cx="450" cy="540" r="1"/>
|
||||
<circle cx="580" cy="520" r="1.2"/><circle cx="680" cy="560" r="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Beach grass -->
|
||||
<g fill="none" stroke="#6B8E23" stroke-width="2" stroke-linecap="round" opacity="0.7">
|
||||
<path d="M45,445 Q48,425 52,445"/>
|
||||
<path d="M52,443 Q56,418 60,443"/>
|
||||
<path d="M40,447 Q42,430 44,447"/>
|
||||
<path d="M58,445 Q62,428 64,447"/>
|
||||
<path d="M735,455 Q738,435 741,455"/>
|
||||
<path d="M741,453 Q745,430 748,455"/>
|
||||
<path d="M730,457 Q732,440 734,457"/>
|
||||
</g>
|
||||
|
||||
<!-- Shadow under bicycle on sand -->
|
||||
<ellipse cx="370" cy="470" rx="95" ry="10" fill="rgba(0,0,0,0.08)"/>
|
||||
|
||||
<!-- ===== BICYCLE ===== -->
|
||||
<g transform="translate(305, 380)">
|
||||
<!-- Back wheel -->
|
||||
<circle cx="0" cy="55" r="46" fill="none" stroke="#212121" stroke-width="3.5"/>
|
||||
<circle cx="0" cy="55" r="42" fill="none" stroke="#424242" stroke-width="0.8" opacity="0.4"/>
|
||||
<!-- Back spokes -->
|
||||
<g stroke="#9E9E9E" stroke-width="0.8" opacity="0.7">
|
||||
<line x1="0" y1="13" x2="0" y2="97"/><line x1="-42" y1="55" x2="42" y2="55"/>
|
||||
<line x1="-30" y1="25" x2="30" y2="85"/><line x1="-30" y1="85" x2="30" y2="25"/>
|
||||
<line x1="-42" y1="35" x2="42" y2="75"/><line x1="-42" y1="75" x2="42" y2="35"/>
|
||||
</g>
|
||||
<circle cx="0" cy="55" r="5" fill="#616161"/>
|
||||
<circle cx="0" cy="55" r="2" fill="#9E9E9E"/>
|
||||
|
||||
<!-- Front wheel -->
|
||||
<circle cx="130" cy="55" r="46" fill="none" stroke="#212121" stroke-width="3.5"/>
|
||||
<circle cx="130" cy="55" r="42" fill="none" stroke="#424242" stroke-width="0.8" opacity="0.4"/>
|
||||
<!-- Front spokes -->
|
||||
<g stroke="#9E9E9E" stroke-width="0.8" opacity="0.7">
|
||||
<line x1="130" y1="13" x2="130" y2="97"/><line x1="88" y1="55" x2="172" y2="55"/>
|
||||
<line x1="100" y1="25" x2="160" y2="85"/><line x1="100" y1="85" x2="160" y2="25"/>
|
||||
<line x1="88" y1="35" x2="172" y2="75"/><line x1="88" y1="75" x2="172" y2="35"/>
|
||||
</g>
|
||||
<circle cx="130" cy="55" r="5" fill="#616161"/>
|
||||
<circle cx="130" cy="55" r="2" fill="#9E9E9E"/>
|
||||
|
||||
<!-- Frame - main triangle -->
|
||||
<line x1="0" y1="55" x2="25" y2="8" stroke="#C62828" stroke-width="4.5" stroke-linecap="round"/>
|
||||
<line x1="25" y1="8" x2="100" y2="14" stroke="#C62828" stroke-width="4.5" stroke-linecap="round"/>
|
||||
<line x1="100" y1="14" x2="0" y2="55" stroke="#C62828" stroke-width="4" stroke-linecap="round"/>
|
||||
<!-- Chain stay -->
|
||||
<line x1="0" y1="55" x2="68" y2="55" stroke="#C62828" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- Seat stay -->
|
||||
<line x1="25" y1="8" x2="68" y2="55" stroke="#C62828" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Fork -->
|
||||
<path d="M100,14 Q108,30 130,55" fill="none" stroke="#C62828" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- Down tube to bottom bracket connection highlight -->
|
||||
<circle cx="68" cy="55" r="3" fill="#B71C1C"/>
|
||||
|
||||
<!-- Seat post -->
|
||||
<line x1="25" y1="8" x2="23" y2="-8" stroke="#424242" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Seat -->
|
||||
<path d="M10,-11 Q23,-16 38,-11 Q36,-7 23,-5 Q10,-7 10,-11 Z" fill="#333"/>
|
||||
|
||||
<!-- Handlebar stem -->
|
||||
<line x1="96" y1="14" x2="104" y2="-8" stroke="#424242" stroke-width="3" stroke-linecap="round"/>
|
||||
<!-- Handlebars -->
|
||||
<path d="M92,-6 Q104,-18 118,-8" fill="none" stroke="#424242" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- Handlebar grips -->
|
||||
<line x1="89" y1="-4" x2="93" y2="-7" stroke="#212121" stroke-width="5" stroke-linecap="round"/>
|
||||
<line x1="115" y1="-6" x2="119" y2="-5" stroke="#212121" stroke-width="5" stroke-linecap="round"/>
|
||||
|
||||
<!-- Crank and pedals -->
|
||||
<circle cx="68" cy="55" r="9" fill="none" stroke="#555" stroke-width="2"/>
|
||||
<line x1="68" y1="44" x2="68" y2="66" stroke="#555" stroke-width="3.5" stroke-linecap="round"/>
|
||||
<!-- Pedals -->
|
||||
<rect x="59" y="40" width="18" height="5" rx="2" fill="#424242"/>
|
||||
<rect x="59" y="63" width="18" height="5" rx="2" fill="#424242"/>
|
||||
<rect x="59" y="40" width="18" height="2" rx="1" fill="#616161" opacity="0.5"/>
|
||||
<rect x="59" y="63" width="18" height="2" rx="1" fill="#616161" opacity="0.5"/>
|
||||
|
||||
<!-- Chain (simplified) -->
|
||||
<path d="M68,55 Q34,72 0,55" fill="none" stroke="#555" stroke-width="1.8" stroke-dasharray="3,2" opacity="0.5"/>
|
||||
<!-- Chainring -->
|
||||
<circle cx="68" cy="55" r="11" fill="none" stroke="#666" stroke-width="1" stroke-dasharray="2,3" opacity="0.4"/>
|
||||
</g>
|
||||
|
||||
<!-- ===== PELICAN ===== -->
|
||||
<g transform="translate(330, 310)">
|
||||
<!-- Legs with orange-yellow color -->
|
||||
<!-- Left leg (reaching to upper pedal) -->
|
||||
<path d="M-2,68 Q-8,80 -12,88 Q-14,92 -18,94" fill="none" stroke="#E8A020" stroke-width="5" stroke-linecap="round"/>
|
||||
<!-- Right leg (reaching to lower pedal) -->
|
||||
<path d="M6,68 Q12,82 16,95 Q18,100 22,108" fill="none" stroke="#E8A020" stroke-width="5" stroke-linecap="round"/>
|
||||
|
||||
<!-- Feet - left -->
|
||||
<g transform="translate(-18,94)">
|
||||
<path d="M0,0 L-8,6 M0,0 L-5,8 M0,0 L0,8" fill="none" stroke="#E8A020" stroke-width="3.5" stroke-linecap="round"/>
|
||||
</g>
|
||||
<!-- Feet - right -->
|
||||
<g transform="translate(22,108)">
|
||||
<path d="M0,0 L-8,6 M0,0 L-5,8 M0,0 L0,8" fill="none" stroke="#E8A020" stroke-width="3.5" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Tail feathers -->
|
||||
<path d="M-28,55 L-52,60 Q-55,62 -50,64 L-25,58 Z" fill="#E0D8C4"/>
|
||||
<path d="M-30,58 L-55,66 Q-58,68 -52,70 L-27,62 Z" fill="#D8CFC0"/>
|
||||
<path d="M-28,62 L-48,72 Q-50,74 -46,74 L-24,65 Z" fill="#D0C8B8"/>
|
||||
|
||||
<!-- Body -->
|
||||
<ellipse cx="0" cy="38" rx="42" ry="40" fill="url(#bodyGrad)" stroke="#DDD8C8" stroke-width="0.8"/>
|
||||
<!-- Body highlight -->
|
||||
<ellipse cx="-5" cy="28" rx="28" ry="25" fill="white" opacity="0.25"/>
|
||||
<!-- Body bottom shading -->
|
||||
<ellipse cx="5" cy="52" rx="30" ry="18" fill="#E8E0D0" opacity="0.3"/>
|
||||
|
||||
<!-- Wing -->
|
||||
<path d="M-12,22 Q-48,32 -44,58 Q-42,72 -38,76 Q-30,82 -12,78 Q0,72 6,60 Q12,48 8,30 Z" fill="#E2DACE" stroke="#CFC6B0" stroke-width="0.8"/>
|
||||
<!-- Wing feather lines -->
|
||||
<g fill="none" stroke="#CFC6B0" stroke-width="0.8" opacity="0.6">
|
||||
<path d="M-35,42 Q-40,52 -37,68"/>
|
||||
<path d="M-28,36 Q-33,50 -28,70"/>
|
||||
<path d="M-20,32 Q-24,48 -18,72"/>
|
||||
<path d="M-12,28 Q-15,46 -10,72"/>
|
||||
</g>
|
||||
<!-- Wing highlight -->
|
||||
<path d="M-30,38 Q-35,48 -32,58" fill="none" stroke="#F0E8D8" stroke-width="1.5" opacity="0.4"/>
|
||||
|
||||
<!-- Neck - white with slight curve -->
|
||||
<path d="M18,15 Q30,2 38,-10 Q42,-18 40,-25 Q38,-30 35,-32" fill="url(#bodyGrad)" stroke="#DDD8C8" stroke-width="0.5"/>
|
||||
<path d="M12,18 Q22,8 32,-5 Q38,-14 38,-25" fill="url(#bodyGrad)"/>
|
||||
<!-- Neck back line -->
|
||||
<path d="M22,12 Q34,0 40,-10 Q43,-18 41,-25" fill="none" stroke="#DDD8C8" stroke-width="0.5"/>
|
||||
|
||||
<!-- Head -->
|
||||
<ellipse cx="36" cy="-34" rx="20" ry="17" fill="url(#bodyGrad)" stroke="#DDD8C8" stroke-width="0.5"/>
|
||||
<!-- Head highlight -->
|
||||
<ellipse cx="33" cy="-38" rx="10" ry="8" fill="white" opacity="0.15"/>
|
||||
|
||||
<!-- Eye ring -->
|
||||
<circle cx="30" cy="-37" r="6" fill="#F5F0E0" stroke="#D0C8B0" stroke-width="0.5"/>
|
||||
<!-- Eye -->
|
||||
<circle cx="30" cy="-37" r="4" fill="#1a1a1a"/>
|
||||
<!-- Eye highlight -->
|
||||
<circle cx="28.5" cy="-38.5" r="1.5" fill="white"/>
|
||||
|
||||
<!-- Upper beak -->
|
||||
<path d="M52,-40 L100,-37 Q104,-36 103,-33 L52,-30 Q49,-34 52,-40 Z" fill="#E88A10" stroke="#D07508" stroke-width="0.5"/>
|
||||
<!-- Beak ridge line -->
|
||||
<line x1="55" y1="-35" x2="98" y2="-35" stroke="#D07508" stroke-width="0.5" opacity="0.4"/>
|
||||
<!-- Beak tip hook -->
|
||||
<path d="M100,-37 Q106,-38 105,-33 Q104,-31 103,-33" fill="#D07508"/>
|
||||
|
||||
<!-- Lower beak with pouch -->
|
||||
<path d="M52,-30 L82,-28 Q88,-18 80,-10 Q70,-4 55,-16 Q51,-22 52,-30 Z" fill="url(#pouchG)" stroke="#D07508" stroke-width="0.5"/>
|
||||
<!-- Pouch texture lines -->
|
||||
<g fill="none" stroke="#D07508" stroke-width="0.4" opacity="0.4">
|
||||
<path d="M56,-24 Q65,-14 72,-16"/>
|
||||
<path d="M58,-22 Q68,-10 76,-14"/>
|
||||
<path d="M60,-20 Q70,-8 78,-12"/>
|
||||
</g>
|
||||
<!-- Pouch highlight -->
|
||||
<path d="M58,-26 Q66,-16 74,-18" fill="none" stroke="#FFB74D" stroke-width="1" opacity="0.3"/>
|
||||
|
||||
<!-- Crown feathers (crest) -->
|
||||
<g fill="none" stroke="#C8BCA5" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M24,-48 Q26,-56 28,-50"/>
|
||||
<path d="M28,-49 Q31,-58 33,-51"/>
|
||||
<path d="M32,-49 Q34,-56 36,-50"/>
|
||||
<path d="M36,-47 Q38,-54 39,-48"/>
|
||||
</g>
|
||||
|
||||
<!-- Cheek pouch coloring -->
|
||||
<ellipse cx="40" cy="-28" rx="8" ry="5" fill="#FFE0B2" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<!-- Pelican shadow on beach -->
|
||||
<ellipse cx="365" cy="468" rx="30" ry="6" fill="rgba(0,0,0,0.06)"/>
|
||||
|
||||
<!-- Wind/motion lines -->
|
||||
<g stroke="white" stroke-width="1.5" stroke-linecap="round" opacity="0.25">
|
||||
<line x1="180" y1="320" x2="230" y2="318"/>
|
||||
<line x1="170" y1="340" x2="215" y2="339"/>
|
||||
<line x1="160" y1="358" x2="200" y2="357"/>
|
||||
<line x1="190" y1="375" x2="225" y2="374"/>
|
||||
</g>
|
||||
|
||||
<!-- Light sparkles on water -->
|
||||
<g fill="white" opacity="0.4">
|
||||
<circle cx="100" cy="300" r="2"><animate attributeName="opacity" values="0.4;0.1;0.4" dur="3s" repeatCount="indefinite"/></circle>
|
||||
<circle cx="250" cy="320" r="1.5"><animate attributeName="opacity" values="0.1;0.4;0.1" dur="2.5s" repeatCount="indefinite"/></circle>
|
||||
<circle cx="500" cy="290" r="2"><animate attributeName="opacity" values="0.3;0.1;0.3" dur="4s" repeatCount="indefinite"/></circle>
|
||||
<circle cx="680" cy="310" r="1.5"><animate attributeName="opacity" values="0.1;0.5;0.1" dur="3.5s" repeatCount="indefinite"/></circle>
|
||||
<circle cx="400" cy="340" r="1"><animate attributeName="opacity" values="0.4;0.15;0.4" dur="2s" repeatCount="indefinite"/></circle>
|
||||
<circle cx="150" cy="350" r="1.5"><animate attributeName="opacity" values="0.2;0.5;0.2" dur="3s" repeatCount="indefinite"/></circle>
|
||||
</g>
|
||||
|
||||
<!-- Tiny sailboat in distance -->
|
||||
<g transform="translate(560, 270)" opacity="0.5">
|
||||
<path d="M0,10 L3,-12 L6,10 Z" fill="white"/>
|
||||
<line x1="3" y1="-12" x2="3" y2="12" stroke="#555" stroke-width="0.8"/>
|
||||
<path d="M-2,10 Q3,14 8,10" fill="#5DADE2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
Reference in new issue
Block a user