Ox Alpha Released: Specs, Benchmarks & Interactive Coding Tests
Ox Alpha released today, August 21, 2026. Featuring advanced spatial reasoning and ultra-long context comprehension, we run hands-on photorealistic SVG, 3D procedural globe, and 3D WebGL wizard adventure game benchmarks.
Mohid Mirza
Co-Founder & Lead Programmer of AcceleratedLogic AI
Architecture, Specs & Capabilities
Test 1: Photorealistic 3D Glass Sphere with Origami Crane
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Folded Light — a paper crane in glass</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..600;1,9..144,300..600&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{background:#0b0a08;display:flex;align-items:center;justify-content:center;overflow:hidden;
font-family:'Fraunces',Georgia,serif;color:#ece3cf}
.stage{position:relative;width:min(100vw,calc(100vh*1.3953));aspect-ratio:1200/860;
animation:rise 1.3s cubic-bezier(.22,.61,.21,1) both}
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
svg{width:100%;height:100%;display:block;cursor:grab;touch-action:none;outline:none}
svg.dragging{cursor:grabbing}
.plate{position:absolute;left:0;right:0;bottom:2.8%;text-align:center;pointer-events:none}
.rule{width:26px;height:1px;background:#4a4437;margin:0 auto 13px}
.over{font-size:10px;letter-spacing:.34em;text-transform:uppercase;color:#877f6d;margin-bottom:11px}
h1{font-style:italic;font-weight:450;font-size:clamp(22px,3vw,34px);letter-spacing:.01em;color:#ece3cf}
.hint{margin-top:11px;display:flex;align-items:center;justify-content:center;gap:7px;font-size:10px;
letter-spacing:.22em;text-transform:uppercase;color:#6f6858;transition:opacity 1.2s ease}
.stage.dragged .hint{opacity:0}
.corner{position:absolute;font-size:9px;letter-spacing:.26em;text-transform:uppercase;color:#57513f}
.tl{top:3.6%;left:3.4%}.tr{top:3.6%;right:3.4%}
@media (prefers-reduced-motion:reduce){.stage{animation:none}}
</style>
</head>
<body>
<div class="stage" id="stage">
<svg id="scene" viewBox="0 0 1200 860" tabindex="0" role="img"
aria-label="A glass sphere with an origami crane suspended inside. Drag to rotate the crane.">
<title>Folded Light — a paper crane in glass</title>
<defs>
<!-- environment -->
<linearGradient id="gBg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#16130f"/><stop offset="1" stop-color="#0b0a08"/>
</linearGradient>
<radialGradient id="gGlow">
<stop offset="0" stop-color="#ffd9a8" stop-opacity="0.11"/>
<stop offset="0.6" stop-color="#ffd9a8" stop-opacity="0.04"/>
<stop offset="1" stop-color="#ffd9a8" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gGround" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e8cf9f" stop-opacity="0.06"/>
<stop offset="1" stop-color="#e8cf9f" stop-opacity="0"/>
</linearGradient>
<!-- drop shadow & caustic -->
<radialGradient id="gShadow">
<stop offset="0" stop-color="#000" stop-opacity="0.62"/>
<stop offset="0.5" stop-color="#000" stop-opacity="0.28"/>
<stop offset="1" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gCaustic">
<stop offset="0" stop-color="#ffdfae" stop-opacity="0.8"/><stop offset="1" stop-color="#ffdfae" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gCausticC">
<stop offset="0" stop-color="#fff4de" stop-opacity="0.95"/><stop offset="1" stop-color="#fff4de" stop-opacity="0"/>
</radialGradient>
<!-- floor reflection -->
<radialGradient id="gRefl">
<stop offset="0" stop-color="#a9c4cc" stop-opacity="0.16"/>
<stop offset="0.7" stop-color="#a9c4cc" stop-opacity="0.05"/>
<stop offset="1" stop-color="#a9c4cc" stop-opacity="0"/>
</radialGradient>
<linearGradient id="gReflFade" gradientUnits="userSpaceOnUse" x1="0" y1="746" x2="0" y2="1000">
<stop offset="0" stop-color="#fff" stop-opacity="0.75"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<mask id="mRefl" maskUnits="userSpaceOnUse" x="0" y="740" width="1200" height="280">
<rect x="0" y="740" width="1200" height="280" fill="url(#gReflFade)"/>
</mask>
<!-- interior light -->
<radialGradient id="gSheen">
<stop offset="0" stop-color="#fff" stop-opacity="0.07"/><stop offset="1" stop-color="#fff" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gTrans">
<stop offset="0" stop-color="#ffdfae" stop-opacity="0.14"/><stop offset="1" stop-color="#ffdfae" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gTransC">
<stop offset="0" stop-color="#fff1da" stop-opacity="0.20"/><stop offset="1" stop-color="#fff1da" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gInSh">
<stop offset="0" stop-color="#000" stop-opacity="0.30"/><stop offset="1" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gInSh2">
<stop offset="0" stop-color="#000" stop-opacity="0.15"/><stop offset="1" stop-color="#000" stop-opacity="0"/>
</radialGradient>
<!-- glass body: transparent core, thick cool rim -->
<radialGradient id="gTint">
<stop offset="0" stop-color="#cfe8ea" stop-opacity="0.02"/>
<stop offset="0.55" stop-color="#c2e0e4" stop-opacity="0.045"/>
<stop offset="0.8" stop-color="#a8d2d8" stop-opacity="0.11"/>
<stop offset="0.94" stop-color="#8fc4cf" stop-opacity="0.22"/>
<stop offset="1" stop-color="#7fb9c8" stop-opacity="0.32"/>
</radialGradient>
<linearGradient id="gRim" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.95"/>
<stop offset="0.42" stop-color="#d8ecee" stop-opacity="0.30"/>
<stop offset="0.74" stop-color="#ffffff" stop-opacity="0.08"/>
<stop offset="1" stop-color="#ffdfb0" stop-opacity="0.40"/>
</linearGradient>
<!-- film grain fades before the frame edge -->
<radialGradient id="gMaskGrain" gradientUnits="userSpaceOnUse" cx="600" cy="420" r="680">
<stop offset="0" stop-color="#fff"/><stop offset="0.62" stop-color="#fff"/><stop offset="1" stop-color="#000"/>
</radialGradient>
<mask id="mGrain" maskUnits="userSpaceOnUse" x="0" y="0" width="1200" height="860">
<rect width="1200" height="860" fill="url(#gMaskGrain)"/>
</mask>
<!-- refraction: ray-bent displacement map (href baked in by JS) + softening -->
<filter id="fRefract" filterUnits="userSpaceOnUse" x="330" y="142" width="540" height="540"
color-interpolation-filters="sRGB">
<feImage id="refrMap" x="330" y="142" width="540" height="540" preserveAspectRatio="none" result="map"/>
<feDisplacementMap in="SourceGraphic" in2="map" scale="64" xChannelSelector="R" yChannelSelector="G"/>
<feGaussianBlur stdDeviation="0.35"/>
</filter>
<!-- static paper tooth inside the sphere -->
<filter id="fPaper" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="2" seed="8"/>
<feColorMatrix type="matrix"
values="0 0 0 0 0.35 0 0 0 0 0.30 0 0 0 0 0.24 0.35 0.35 0.35 0 -0.42"/>
</filter>
<!-- scene-wide film grain -->
<filter id="fGrain" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2" seed="4"/>
<feColorMatrix type="matrix"
values="0 0 0 0 0.90 0 0 0 0 0.87 0 0 0 0 0.80 0.35 0.35 0.35 0 -0.42"/>
</filter>
<filter id="b1" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="0.7"/></filter>
<filter id="b2" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="1.6"/></filter>
<filter id="b3" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="3"/></filter>
<filter id="b4" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="4.5"/></filter>
<filter id="b5" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="6"/></filter>
<filter id="b6" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="8"/></filter>
<filter id="b8" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="12"/></filter>
<clipPath id="clipSphere"><circle cx="600" cy="412" r="270"/></clipPath>
<rect id="specPill" x="-78" y="-27" width="156" height="54" rx="27"/>
</defs>
<!-- ======== environment ======== -->
<rect width="1200" height="860" fill="url(#gBg)"/>
<circle cx="545" cy="330" r="440" fill="url(#gGlow)"/>
<rect x="0" y="744" width="1200" height="116" fill="url(#gGround)"/>
<rect x="0" y="743.6" width="1200" height="1" fill="#ffe8c8" opacity="0.05"/>
<!-- drop shadow, then caustic focused through the ball -->
<ellipse cx="600" cy="752" rx="305" ry="42" fill="url(#gShadow)" opacity="0.85"/>
<ellipse cx="600" cy="746" rx="150" ry="18" fill="url(#gShadow)"/>
<ellipse cx="614" cy="743" rx="118" ry="14" fill="url(#gCaustic)" opacity="0.8"/>
<ellipse cx="616" cy="742.5" rx="44" ry="6.5" fill="url(#gCausticC)" opacity="0.9"/>
<ellipse cx="618" cy="742" rx="16" ry="3" fill="#fff6e6" opacity="0.9" filter="url(#b1)"/>
<!-- floor reflection (mirrored, blurred, fading) -->
<g mask="url(#mRefl)" opacity="0.55">
<g transform="translate(0,1488) scale(1,-1)" filter="url(#b3)">
<circle cx="600" cy="412" r="270" fill="url(#gRefl)"/>
<use href="#specPill" transform="translate(487,290) rotate(-17)" fill="#ffffff" opacity="0.14"/>
<circle cx="463" cy="267" r="10" fill="#fff" opacity="0.16"/>
</g>
</g>
<!-- ======== glass interior ======== -->
<circle cx="520" cy="345" r="235" fill="url(#gSheen)"/>
<circle cx="676" cy="497" r="205" fill="url(#gTrans)"/>
<circle cx="690" cy="508" r="48" fill="url(#gTransC)"/>
<!-- crane (refracted) with its inner cast shadow -->
<g clip-path="url(#clipSphere)">
<ellipse cx="656" cy="476" rx="185" ry="72" fill="url(#gInSh2)"/>
<ellipse cx="638" cy="470" rx="128" ry="50" fill="url(#gInSh)"/>
<g id="crane" filter="url(#fRefract)"></g>
<circle cx="600" cy="412" r="268" filter="url(#fPaper)" opacity="0.4"/>
</g>
<!-- ======== glass surface ======== -->
<g clip-path="url(#clipSphere)">
<circle cx="600" cy="412" r="270" fill="url(#gTint)"/>
<circle cx="600" cy="412" r="251" fill="none" stroke="#07161a" stroke-opacity="0.30" stroke-width="16" filter="url(#b6)"/>
<circle cx="600" cy="412" r="243" fill="none" stroke="#cfeef2" stroke-opacity="0.10" stroke-width="1.6"/>
<path id="aRing" fill="none" stroke="#ffe6bd" stroke-opacity="0.30" stroke-width="9" filter="url(#b6)"/>
<path id="aStUR" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="16" filter="url(#b8)"/>
<path id="aStLL" fill="none" stroke="#ffc98f" stroke-opacity="0.06" stroke-width="18" filter="url(#b8)"/>
<path id="aCntSoft" fill="none" stroke="#ffd9a6" stroke-opacity="0.20" stroke-width="6" filter="url(#b5)"/>
<path id="aCnt" fill="none" stroke="#ffe2b8" stroke-opacity="0.55" stroke-width="1.6" stroke-linecap="round" filter="url(#b1)"/>
<path id="aRimSoft" fill="none" stroke="#ffffff" stroke-opacity="0.30" stroke-width="8" filter="url(#b5)"/>
<path id="aFrW" fill="none" stroke="#ff8a50" stroke-opacity="0.26" stroke-width="1.1" filter="url(#b1)"/>
<path id="aFrC" fill="none" stroke="#7fd8ff" stroke-opacity="0.20" stroke-width="1" filter="url(#b1)"/>
<g id="specShift">
<use href="#specPill" transform="translate(487,290) rotate(-17)" fill="#ffffff" opacity="0.30" filter="url(#b6)"/>
<use href="#specPill" transform="translate(487,290) rotate(-17) scale(0.78)" fill="#ffffff" opacity="0.30" filter="url(#b2)"/>
<circle cx="463" cy="267" r="10" fill="#fff" opacity="0.75" filter="url(#b2)"/>
<circle cx="463" cy="267" r="4" fill="#fff" opacity="0.95"/>
<use href="#specPill" transform="translate(716,540) rotate(-17)" fill="#ffe9c8" opacity="0.10" filter="url(#b5)"/>
<circle cx="748" cy="300" r="2.2" fill="#fff" opacity="0.45"/>
</g>
<path id="aRimMain" fill="none" stroke="#ffffff" stroke-opacity="0.9" stroke-width="2.4" stroke-linecap="round" filter="url(#b1)"/>
</g>
<path id="aOutGlow" fill="none" stroke="#ffffff" stroke-opacity="0.16" stroke-width="3.5" filter="url(#b4)"/>
<circle cx="600" cy="412" r="270" fill="none" stroke="url(#gRim)" stroke-width="1.5" opacity="0.95"/>
<rect width="1200" height="860" filter="url(#fGrain)" mask="url(#mGrain)" opacity="0.35"/>
</svg>
<div class="corner tl">Soda-lime glass · n 1.52</div>
<div class="corner tr">Kami 60 g/m² · 28 facets</div>
<div class="plate">
<div class="rule"></div>
<div class="over">Still life · glass & paper</div>
<h1>Folded Light</h1>
<div class="hint">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.9-6.6"/><polyline points="21 3 21 9 15 9"/></svg>
<span>Drag to turn the crane</span>
</div>
</div>
</div>
<script>
(()=>{
const $=id=>document.getElementById(id);
const CX=600,CY=412,R=270;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
/* ---------- rim / reflection arc paths ---------- */
const T=Math.PI/180;
const arc=(r,a0,a1)=>{
const x0=CX+r*Math.cos(a0*T),y0=CY+r*Math.sin(a0*T),
x1=CX+r*Math.cos(a1*T),y1=CY+r*Math.sin(a1*T);
return `M ${x0.toFixed(1)} ${y0.toFixed(1)} A ${r} ${r} 0 ${a1-a0>180?1:0} 1 ${x1.toFixed(1)} ${y1.toFixed(1)}`;
};
const ARCS={aRimSoft:[262,168,292],aRimMain:[267,170,290],aCntSoft:[260,8,112],aCnt:[266,14,104],
aFrW:[268.6,172,288],aFrC:[265.4,176,284],aRing:[212,14,106],
aStUR:[195,298,342],aStLL:[190,118,162],aOutGlow:[272.5,175,285]};
for(const k in ARCS){const[r,a,b]=ARCS[k];$(k).setAttribute('d',arc(r,a,b));}
/* ---------- refraction map ---------- */
(()=>{
const S=540,cv=document.createElement('canvas');cv.width=cv.height=S;
const ctx=cv.getContext('2d'),im=ctx.createImageData(S,S),d=im.data;
const eta=1/1.32,K=64,GAIN=0.62,PLANE=-0.03;
for(let py=0;py<S;py++){
const v=(py+0.5)/S*2-1;
for(let px=0;px<S;px++){
const u=(px+0.5)/S*2-1,i=(py*S+px)*4;
let ox=0,oy=0;const r2=u*u+v*v;
if(r2<1){
const z=Math.sqrt(1-r2),k=1-eta*eta*(1-z*z);
if(k>0){
const cT=Math.sqrt(k),f=eta*z-cT;
const tx=f*u,ty=f*v,tz=-eta+f*z,s=(PLANE-z)/tz;
ox=s*tx*R*GAIN; oy=s*ty*R*GAIN;
}
}
d[i] =clamp(128+ox*255/K,0,255);
d[i+1]=clamp(128+oy*255/K,0,255);
d[i+2]=128;d[i+3]=255;
}
}
ctx.putImageData(im,0,0);
$('refrMap').setAttribute('href',cv.toDataURL());
})();
/* ---------- origami crane: hand-built 3D mesh ---------- */
const VT={
Ct:[0.02,0.62,0], Cf:[0.62,0.18,0], Cb:[-0.62,0.22,0], Cq:[0.02,-0.50,0],
Nt:[1.30,0.66,0], Ht:[1.45,0.80,0.05], Hm:[1.52,0.68,0.03], Bk:[1.63,0.54,0],
Tt:[-1.42,0.95,0], Pn:[0.00,0.20,0.30],
W1:[0.34,0.72,0.62], W2:[-0.38,0.78,0.68], Tw:[-0.16,1.42,1.10],
A1:[0.46,0.06,0.05], A2:[0.12,-0.38,0.05], Am:[0.88,0.36,0.10],
B1:[-0.12,-0.38,0.05], B2:[-0.46,0.06,0.05], Bm:[-0.82,0.33,0.10]
};
for(const k in VT){const v=VT[k];VT['m'+k]=[v[0],v[1],-v[2]];}
const FACES=[
['Ct','Cf','Pn',0,0],['Cf','Cq','Pn',0,0],['Cq','Cb','Pn',0,0],['Cb','Ct','Pn',0,0],
['Cf','Pn','W1',1,0.02],['Cf','W1','Tw',1,0.02],['Pn','Cb','W2',1,0.02],['Pn','W2','Tw',1,0.02],
['A1','Am','A2',2,0.01],['Am','Nt','A2',2,0.01],
['B1','Bm','B2',3,0.01],['Bm','Tt','B2',3,0.01],
['Nt','Hm','Bk',4,0.03],['Hm','Ht','Bk',4,0.03],
['mCt','mCf','mPn',0,0],['mCf','mCq','mPn',0,0],['mCq','mCb','mPn',0,0],['mCb','mCt','mPn',0,0],
['mCf','mPn','mW1',1,-0.02],['mCf','mW1','mTw',1,-0.02],['mPn','mCb','mW2',1,-0.02],['mPn','mW2','mTw',1,-0.02],
['mA1','mAm','mA2',2,-0.01],['mAm','mNt','mA2',2,-0.01],
['mB1','mBm','mB2',3,-0.01],['mBm','mTt','mB2',3,-0.01],
['mNt','mHm','mBk',4,-0.03],['mHm','mHt','mBk',4,-0.03]
];
const CREASES=[
['Cf','W1',4],['Pn','W2',6],['mCf','mW1',18],['mPn','mW2',20],
['Cf','Ct',0],['Ct','Cb',3],['mCf','mCt',14],['mCt','mCb',17]
];
const PX=0.08,PY=0.42,PZ=0,FOC=7.5,SC=R*0.362;
const n3=(x,y,z)=>{const l=Math.hypot(x,y,z)||1;return[x/l,y/l,z/l];};
const L =n3(-0.55, 0.72,0.42);
const LF=n3( 0.65,-0.30,0.45);
const H =n3(L[0],L[1],L[2]+1);
let LSX=L[0],LSY=-L[1];{const l=Math.hypot(LSX,LSY);LSX/=l;LSY/=l;}
function shade(n,back){
const dl=Math.max(n[0]*L[0]+n[1]*L[1]+n[2]*L[2],0);
const fl=Math.max(n[0]*LF[0]+n[1]*LF[1]+n[2]*LF[2],0);
const sp=Math.pow(Math.max(n[0]*H[0]+n[1]*H[1]+n[2]*H[2],0),26);
const b=back?[236,225,203]:[250,245,233];
const s=0.40+0.60*dl;
return[Math.min(255,b[0]*s+fl*22+sp*72),
Math.min(255,b[1]*s+fl*24+sp*72),
Math.min(255,b[2]*s+fl*30+sp*72)];
}
let yaw=-0.55,pitch=0.16,vy=0,vp=0,drag=false,lx=0,ly=0;
const crane=$('crane'),specShift=$('specShift'),scene=$('scene'),stage=$('stage');
const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
function render(){
const cyw=Math.cos(yaw),syw=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch);
const P={};
for(const k in VT){
const v=VT[k],x=v[0]-PX,y=v[1]-PY,z=v[2]-PZ;
const x1=x*cyw+z*syw, z1=z*cyw-x*syw;
const y2=y*cp-z1*sp, z2=y*sp+z1*cp;
const pe=FOC/(FOC-z2);
P[k]=[x1,y2,z2, CX+x1*pe*SC, CY-y2*pe*SC];
}
const N=FACES.map(f=>{
const a=P[f[0]],b=P[f[1]],c=P[f[2]];
const ux=b[0]-a[0],uy=b[1]-a[1],uz=b[2]-a[2];
const vx=c[0]-a[0],wy=c[1]-a[1],wz=c[2]-a[2];
let nx=uy*wz-uz*wy, ny=uz*vx-ux*wz, nz=ux*wy-uy*vx;
const l=Math.hypot(nx,ny,nz)||1;nx/=l;ny/=l;nz/=l;
let back=false;if(nz<0){nx=-nx;ny=-ny;nz=-nz;back=true;}
return{n:[nx,ny,nz],back,d:(a[2]+b[2]+c[2])/3+f[4]};
});
const order=[...FACES.keys()].sort((p,q)=>N[p].d-N[q].d);
const defs=[],body=[];
const st='stroke="rgba(56,42,26,0.20)" stroke-width="0.8" stroke-linejoin="round"';
for(const i of order){
const f=FACES[i],a=P[f[0]],b=P[f[1]],c=P[f[2]];
const col=shade(N[i].n,N[i].back).map(Math.round);
const pts=`${a[3].toFixed(1)},${a[4].toFixed(1)} ${b[3].toFixed(1)},${b[4].toFixed(1)} ${c[3].toFixed(1)},${c[4].toFixed(1)}`;
const vs=[a,b,c];let iM=0,iN=0,tM=-1e18,tN=1e18;
vs.forEach((v,vi)=>{const t=v[3]*LSX+v[4]*LSY;if(t>tM){tM=t;iM=vi;}if(t<tN){tN=t;iN=vi;}});
if(tM-tN<1.5){
body.push(`<polygon points="${pts}" fill="rgb(${col})" ${st}/>`);
}else{
const A=vs[iM],B=vs[iN];
const c1=col.map(x=>Math.min(255,Math.round(x*1.05+5)));
const c2=col.map(x=>Math.max(0,Math.round(x*0.85-6)));
defs.push(`<linearGradient id="f${i}" gradientUnits="userSpaceOnUse" x1="${A[3].toFixed(1)}" y1="${A[4].toFixed(1)}" x2="${B[3].toFixed(1)}" y2="${B[4].toFixed(1)}"><stop offset="0" stop-color="rgb(${c1})"/><stop offset="1" stop-color="rgb(${c2})"/></linearGradient>`);
body.push(`<polygon points="${pts}" fill="url(#f${i})" ${st}/>`);
}
}
for(const cr of CREASES){
const nn=N[cr[2]];
if(nn&&!nn.back&&nn.n[2]>0.12){
const p=P[cr[0]],q=P[cr[1]];
body.push(`<line x1="${p[3].toFixed(1)}" y1="${p[4].toFixed(1)}" x2="${q[3].toFixed(1)}" y2="${q[4].toFixed(1)}" stroke="rgba(255,252,240,0.10)" stroke-width="1.2" stroke-linecap="round"/>`);
}
}
crane.innerHTML=`<defs>${defs.join('')}</defs>${body.join('')}`;
specShift.setAttribute('transform',`translate(${(Math.sin(yaw)*6).toFixed(1)} ${(Math.sin(pitch)*3).toFixed(1)})`);
}
/* ---------- interaction: drag to turn, inertia, idle drift ---------- */
scene.addEventListener('pointerdown',e=>{
drag=true;vy=vp=0;lx=e.clientX;ly=e.clientY;
scene.setPointerCapture(e.pointerId);
scene.classList.add('dragging');stage.classList.add('dragged');
});
scene.addEventListener('pointermove',e=>{
if(!drag)return;
const dx=e.clientX-lx,dy=e.clientY-ly;lx=e.clientX;ly=e.clientY;
yaw+=dx*0.0085;pitch=clamp(pitch+dy*0.006,-0.55,0.6);
vy=clamp(vy*0.5+dx*0.0085*0.5,-0.09,0.09);
vp=vp*0.5+dy*0.006*0.5;
});
const endDrag=()=>{drag=false;scene.classList.remove('dragging');};
scene.addEventListener('pointerup',endDrag);
scene.addEventListener('pointercancel',endDrag);
addEventListener('keydown',e=>{
if(e.key==='ArrowLeft')vy=clamp(vy-0.012,-0.09,0.09);
else if(e.key==='ArrowRight')vy=clamp(vy+0.012,-0.09,0.09);
else if(e.key==='ArrowUp')pitch=clamp(pitch+0.06,-0.55,0.6);
else if(e.key==='ArrowDown')pitch=clamp(pitch-0.06,-0.55,0.6);
else return;
e.preventDefault();
});
(function loop(){
if(!drag){
yaw+=vy;pitch=clamp(pitch+vp,-0.55,0.6);
vy*=0.94;vp*=0.90;
pitch+=(0.16-pitch)*0.012;
if(!reduce&&Math.abs(vy)<0.0025)yaw+=0.0032;
}
render();
requestAnimationFrame(loop);
})();
})();
</script>
</body>
</html>
Test 2: 3D Procedural Globe Generation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Terra Synthetica — Procedural Globe</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@1,9..144,300..600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{
--bg:#050608;
--ink:#e9e4d8;
--dim:rgba(233,228,216,.42);
--accent:#e2aa4f;
--hair:rgba(233,228,216,.14);
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:var(--bg)}
body{font-family:'IBM Plex Mono',monospace;color:var(--ink)}
#scene{position:fixed;inset:0}
#scene canvas{display:block}
/* ---------- HUD ---------- */
.hud{position:fixed;z-index:5;user-select:none;-webkit-user-select:none}
.hud-top{top:30px;left:34px;pointer-events:none}
.eyebrow{font-size:10px;letter-spacing:.3em;color:var(--accent)}
h1{font-family:'Fraunces',serif;font-weight:380;font-style:italic;
font-size:clamp(30px,4.4vw,46px);line-height:1.05;margin-top:10px;color:var(--ink)}
.sub{margin-top:9px;font-size:9px;letter-spacing:.22em;color:var(--dim)}
.hud-bottom{left:0;right:0;bottom:0;display:flex;align-items:flex-end;
justify-content:space-between;gap:26px;flex-wrap:wrap;
padding:16px 34px 18px;border-top:1px solid var(--hair);
background:rgba(5,6,8,.35);backdrop-filter:blur(9px);-webkit-backdrop-filter:blur(9px)}
.readouts{display:flex;gap:36px}
.ro .lbl{font-size:9px;letter-spacing:.24em;color:var(--dim);margin-bottom:6px}
.ro .val{font-size:15px;font-variant-numeric:tabular-nums;color:var(--ink)}
.ro .val b{font-weight:400;color:var(--accent)}
.ctrl{display:flex;align-items:center;gap:18px;flex:1;min-width:260px;max-width:420px}
.ctrl .lbl{font-size:9px;letter-spacing:.24em;color:var(--dim);white-space:nowrap}
.ctrl .val{font-size:12px;color:var(--accent);min-width:64px;text-align:right;
font-variant-numeric:tabular-nums}
input[type=range]{-webkit-appearance:none;appearance:none;flex:1;height:20px;
background:transparent;cursor:pointer;--fill:34%}
input[type=range]:focus{outline:none}
input[type=range]::-webkit-slider-runnable-track{height:2px;border-radius:1px;
background:linear-gradient(to right,var(--accent) var(--fill),rgba(233,228,216,.18) var(--fill))}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;
border-radius:50%;background:var(--bg);border:1.5px solid var(--accent);
margin-top:-5.5px;box-shadow:0 0 0 3px rgba(226,170,79,.14)}
input[type=range]::-moz-range-track{height:2px;background:rgba(233,228,216,.18)}
input[type=range]::-moz-range-progress{height:2px;background:var(--accent)}
input[type=range]::-moz-range-thumb{width:11px;height:11px;border-radius:50%;
background:var(--bg);border:1.5px solid var(--accent)}
.hint{position:fixed;left:50%;bottom:92px;transform:translateX(-50%);z-index:4;
font-size:9px;letter-spacing:.24em;color:var(--dim);text-transform:uppercase;
pointer-events:none;white-space:nowrap}
.vignette{position:fixed;inset:0;z-index:2;pointer-events:none;
background:radial-gradient(120% 92% at 50% 42%,transparent 56%,rgba(0,0,0,.42) 100%)}
/* ---------- loading veil ---------- */
#veil{position:fixed;inset:0;z-index:20;background:var(--bg);
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;
transition:opacity .7s ease}
#veil.done{opacity:0;pointer-events:none}
#veil .vt{font-family:'Fraunces',serif;font-style:italic;font-weight:380;font-size:22px}
#veil .vs{font-size:9px;letter-spacing:.3em;color:var(--dim)}
#vbar{width:190px;height:1px;background:rgba(233,228,216,.14);position:relative}
#vbar i{position:absolute;left:0;top:-.5px;height:2px;width:0%;background:var(--accent)}
#vpct{font-size:10px;letter-spacing:.24em;color:var(--accent)}
@media(max-width:720px){
.hud-top{top:20px;left:20px}
.sub{display:none}
.readouts{gap:20px}
.hud-bottom{padding:12px 20px 14px;gap:14px}
.hint{bottom:118px}
}
</style>
</head>
<body>
<div id="scene"></div>
<div class="vignette"></div>
<header class="hud hud-top">
<div class="eyebrow">PROCEDURAL BODY · <span id="seed">SEED ----</span></div>
<h1>Terra Synthetica</h1>
<div class="sub">VALUE-NOISE TERRAIN — SYNTHESIZED LOCALLY IN YOUR BROWSER</div>
</header>
<div class="hint">drag to spin · scroll / pinch to zoom · double-click to reset</div>
<footer class="hud hud-bottom">
<div class="readouts">
<div class="ro"><div class="lbl">AXIAL SPIN</div><div class="val"><b id="roSpin">0.0</b> °/s</div></div>
<div class="ro"><div class="lbl">CAMERA RANGE</div><div class="val" id="roDist">3.40 R</div></div>
<div class="ro"><div class="lbl">AXIAL TILT</div><div class="val" id="roTilt">0°</div></div>
</div>
<div class="ctrl">
<span class="lbl">DRIFT RATE</span>
<input type="range" id="speed" min="0" max="100" step="1" value="34" aria-label="Globe rotation speed">
<span class="val" id="speedVal">—</span>
</div>
</footer>
<div id="veil">
<div class="vt">Terra Synthetica</div>
<div id="vbar"><i></i></div>
<div id="vpct">0%</div>
<div class="vs">SYNTHESIZING TERRAIN FIELDS</div>
</div>
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
/* ============================================================
1 · Seeded 3D value noise (sampled on the sphere → seamless)
============================================================ */
const SEED = 1000 + Math.floor(Math.random() * 89999);
document.getElementById('seed').textContent = 'SEED ' + SEED;
function hash3(x, y, z){
let n = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(z, 1440662683) ^ SEED;
n = Math.imul(n ^ (n >>> 13), 1274126177);
n ^= n >>> 16;
return (n >>> 0) / 4294967296;
}
const fade = t => t * t * t * (t * (t * 6 - 15) + 10);
function vnoise(x, y, z){
const X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
const u = fade(x - X), v = fade(y - Y), w = fade(z - Z);
const c000 = hash3(X, Y, Z), c100 = hash3(X+1, Y, Z);
const c010 = hash3(X, Y+1, Z), c110 = hash3(X+1, Y+1, Z);
const c001 = hash3(X, Y, Z+1), c101 = hash3(X+1, Y, Z+1);
const c011 = hash3(X, Y+1, Z+1), c111 = hash3(X+1, Y+1, Z+1);
const x00 = c000 + (c100-c000)*u, x10 = c010 + (c110-c010)*u;
const x01 = c001 + (c101-c001)*u, x11 = c011 + (c111-c011)*u;
const y0 = x00 + (x10-x00)*v, y1 = x01 + (x11-x01)*v;
return y0 + (y1-y0)*w;
}
function fbm(x, y, z, oct){
let s = 0, a = 0.5, n = 0, f = 1;
for(let i = 0; i < oct; i++){ s += a * vnoise(x*f, y*f, z*f); n += a; a *= 0.5; f *= 2; }
return s / n;
}
const sm01 = t => t <= 0 ? 0 : t >= 1 ? 1 : t*t*(3-2*t);
/* ============================================================
2 · Colour ramps
============================================================ */
const OCEAN = [
[0.00,[42,102,104]], [0.08,[20,64,78]],
[0.30,[10,36,50]], [1.00,[4,12,19]]
];
const LAND = [
[0.00,[190,172,134]], [0.035,[150,158,108]], [0.11,[92,120,74]],
[0.30,[62,90,58]], [0.48,[116,103,77]], [0.66,[126,116,99]],
[0.82,[213,209,199]], [1.00,[241,240,236]]
];
function ramp(stops, t, out){
if(t <= stops[0][0]){ out[0]=stops[0][1][0]; out[1]=stops[0][1][1]; out[2]=stops[0][1][2]; return; }
for(let k = 1; k < stops.length; k++){
if(t <= stops[k][0]){
const a = stops[k-1], b = stops[k], u = (t - a[0]) / (b[0] - a[0]);
out[0]=a[1][0]+(b[1][0]-a[1][0])*u;
out[1]=a[1][1]+(b[1][1]-a[1][1])*u;
out[2]=a[1][2]+(b[1][2]-a[1][2])*u;
return;
}
}
const L = stops[stops.length-1][1];
out[0]=L[0]; out[1]=L[1]; out[2]=L[2];
}
/* ============================================================
3 · Procedural map synthesis (chunked so the veil can breathe)
============================================================ */
const SEA = 0.545;
const _c = [0,0,0];
async function generateMaps(onRow){
const W = 1024, H = 512;
const mk = () => { const c = document.createElement('canvas'); c.width=W; c.height=H; return c; };
const cCan = mk(), sCan = mk(), bCan = mk();
const cCtx = cCan.getContext('2d'), sCtx = sCan.getContext('2d'), bCtx = bCan.getContext('2d');
const cI = cCtx.createImageData(W,H), sI = sCtx.createImageData(W,H), bI = bCtx.createImageData(W,H);
const cd = cI.data, sd = sI.data, bd = bI.data;
let row = 0;
while(row < H){
const t0 = performance.now();
while(row < H && performance.now() - t0 < 14){
const j = row++;
const phi = ((j + 0.5) / H) * Math.PI;
const sy = Math.sin(phi), ny = Math.cos(phi);
for(let i = 0; i < W; i++){
const th = ((i + 0.5) / W) * Math.PI * 2;
const nx = sy * Math.cos(th), nz = sy * Math.sin(th);
const q1 = fbm(nx*1.6+5.2, ny*1.6+1.3, nz*1.6+2.8, 2) - 0.5;
const q2 = fbm(nx*1.6+9.1, ny*1.6+6.7, nz*1.6+3.9, 2) - 0.5;
const q3 = fbm(nx*1.6+4.4, ny*1.6+8.8, nz*1.6+7.2, 2) - 0.5;
const px = nx + q1*0.55, py = ny + q2*0.55, pz = nz + q3*0.55;
const base = fbm(px*1.15+3.7, py*1.15+1.9, pz*1.15+8.4, 4);
const det = fbm(px*3.40+7.7, py*3.40+2.1, pz*3.40+5.5, 3);
const e = base*0.66 + det*0.34;
const idx = (j*W + i) * 4;
let r, g, b, spec, bump;
if(e <= SEA){
const d = Math.min(1, (SEA - e) / (SEA * 0.62));
ramp(OCEAN, d, _c);
r=_c[0]; g=_c[1]; b=_c[2];
spec = 225 - d*70; bump = 0;
} else {
let t = (e - SEA) / (0.97 - SEA); if(t > 1) t = 1;
t = Math.pow(t, 0.82);
ramp(LAND, t, _c);
r=_c[0]; g=_c[1]; b=_c[2];
const m = (det - 0.5) * 26; r+=m; g+=m; b+=m;
const an = Math.abs(ny);
if(an > 0.12 && an < 0.62){
const moist = fbm(px*2.2+13.1, py*2.2+4.4, pz*2.2+9.9, 2);
const band = Math.exp(-(((an-0.36)/0.14)**2)) * (1 - moist*1.4);
if(band > 0){
const k = Math.min(0.6, band*0.6);
r+=(172-r)*k; g+=(150-g)*k; b+=(108-b)*k;
}
}
const eff = t + Math.max(0, an - 0.55) * 1.1;
const sk = sm01((eff - 0.72) / 0.16);
if(sk > 0){ r+=(236-r)*sk; g+=(238-g)*sk; b+=(236-b)*sk; }
spec = 26; bump = t*255;
if(an > 0.78){
const cn = fbm(px*4.0+3.3, py*4.0+6.6, pz*4.0+1.1, 2);
const cap = sm01(((an + (cn-0.5)*0.09) - 0.86) / 0.05);
if(cap > 0){
r+=(226-r)*cap; g+=(233-g)*cap; b+=(238-b)*cap;
spec += (135-spec)*cap;
bump = bump*(1-cap) + 200*cap;
}
}
}
cd[idx]=r; cd[idx+1]=g; cd[idx+2]=b; cd[idx+3]=255;
sd[idx]=spec;sd[idx+1]=spec;sd[idx+2]=spec;sd[idx+3]=255;
bd[idx]=bump;bd[idx+1]=bump;bd[idx+2]=bump;bd[idx+3]=255;
}
}
onRow(row / H);
await new Promise(res => requestAnimationFrame(res));
}
cCtx.putImageData(cI,0,0); sCtx.putImageData(sI,0,0); bCtx.putImageData(bI,0,0);
return { cCan, sCan, bCan };
}
async function generateClouds(onRow){
const W = 512, H = 256;
const can = document.createElement('canvas'); can.width=W; can.height=H;
const ctx = can.getContext('2d');
const img = ctx.createImageData(W,H), d = img.data;
let row = 0;
while(row < H){
const t0 = performance.now();
while(row < H && performance.now() - t0 < 14){
const j = row++;
const phi = ((j+0.5)/H) * Math.PI;
const sy = Math.sin(phi), ny = Math.cos(phi);
for(let i = 0; i < W; i++){
const th = ((i+0.5)/W) * Math.PI * 2;
const nx = sy*Math.cos(th), nz = sy*Math.sin(th);
const w = fbm(nx*1.3+21.7, ny*1.3+11.2, nz*1.3+5.8, 2) - 0.5;
const px = nx + w*0.6, py = ny + w*0.6, pz = nz + w*0.6;
const c = fbm(px*2.3, py*1.15, pz*2.3, 4);
let a = sm01((c - 0.50) / 0.17) * 0.9 * (0.35 + 0.65*sy);
const idx = (j*W+i)*4;
d[idx]=250; d[idx+1]=250; d[idx+2]=252; d[idx+3]=a*255;
}
}
onRow(row / H);
await new Promise(res => requestAnimationFrame(res));
}
ctx.putImageData(img,0,0);
return can;
}
let renderer;
try{
renderer = new THREE.WebGLRenderer({ antialias:true });
}catch(err){
document.querySelector('#veil .vs').textContent = 'WEBGL IS NOT AVAILABLE IN THIS BROWSER';
throw err;
}
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
document.getElementById('scene').appendChild(renderer.domElement);
const MAX_ANISO = renderer.capabilities.getMaxAnisotropy();
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050608);
const camera = new THREE.PerspectiveCamera(40, innerWidth/innerHeight, 0.1, 300);
const CAM_DIR = new THREE.Vector3(0, 0.32, 1).normalize();
let dist = 3.4, targetDist = 3.4;
const par = { x:0, y:0, tx:0, ty:0 };
const sun = new THREE.DirectionalLight(0xffd9a6, 3.0);
sun.position.set(-4.5, 2.2, 2.6);
scene.add(sun);
const rimLight = new THREE.DirectionalLight(0x5f7ea6, 1.1);
rimLight.position.set(4, -1.5, -3);
scene.add(rimLight);
scene.add(new THREE.HemisphereLight(0x30414f, 0x0b0a09, 0.55));
const SUN_DIR = sun.position.clone().normalize();
const tiltGroup = new THREE.Group();
scene.add(tiltGroup);
const planet = new THREE.Group();
tiltGroup.add(planet);
const ATMO_VERT = `
varying vec3 vN;
varying vec3 vWN;
void main(){
vN = normalize(normalMatrix * normal);
vWN = normalize(mat3(modelMatrix) * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`;
function atmoMaterial(color, intensity, power, back){
return new THREE.ShaderMaterial({
uniforms:{
uColor:{ value:new THREE.Color(color) },
uSunDir:{ value:SUN_DIR },
uIntensity:{ value:intensity },
uPow:{ value:power }
},
vertexShader: ATMO_VERT,
fragmentShader: `
uniform vec3 uColor; uniform vec3 uSunDir; uniform float uIntensity; uniform float uPow;
varying vec3 vN; varying vec3 vWN;
void main(){
float f = ${back
? 'pow(clamp(-dot(normalize(vN), vec3(0.0,0.0,1.0)) * 1.9, 0.0, 1.0), uPow)'
: 'pow(1.0 - abs(dot(normalize(vN), vec3(0.0,0.0,1.0))), uPow)'};
float day = clamp(dot(normalize(vWN), uSunDir) * 0.6 + 0.45, 0.06, 1.0);
float a = f * day * uIntensity;
gl_FragColor = vec4(uColor * a, a);
}`,
side: back ? THREE.BackSide : THREE.FrontSide,
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending
});
}
function pointsMaterial(scale, twAmp, twSpeed){
return new THREE.ShaderMaterial({
uniforms:{
uTime:{ value:0 }, uScale:{ value:scale },
uPR:{ value:renderer.getPixelRatio() },
uTA:{ value:twAmp }, uTS:{ value:twSpeed }
},
vertexShader:`
attribute float aSize; attribute float aPhase; attribute vec3 aColor;
uniform float uTime, uScale, uPR, uTA, uTS;
varying vec3 vC; varying float vA;
void main(){
vC = aColor;
vec4 mv = modelViewMatrix * vec4(position, 1.0);
vA = 1.0 - uTA * (0.5 + 0.5 * sin(uTime * uTS + aPhase));
gl_PointSize = aSize * uPR * (uScale / -mv.z);
gl_Position = projectionMatrix * mv;
}`,
fragmentShader:`
varying vec3 vC; varying float vA;
void main(){
float d = length(gl_PointCoord - vec2(0.5));
float a = smoothstep(0.5, 0.08, d);
a *= a;
gl_FragColor = vec4(vC, a * vA);
}`,
transparent:true, depthWrite:false, blending:THREE.AdditiveBlending
});
}
let built = false;
let cloudsMesh = null, ringGeo = null, ringMat = null, starMat = null;
const RING_N = 3400;
const rRadius = new Float32Array(RING_N), rAngle = new Float32Array(RING_N),
rY = new Float32Array(RING_N), rSpeed = new Float32Array(RING_N);
function canvasTex(can, srgb){
const t = new THREE.CanvasTexture(can);
if(srgb) t.colorSpace = THREE.SRGBColorSpace;
t.wrapS = THREE.RepeatWrapping;
t.anisotropy = MAX_ANISO;
return t;
}
function buildScene(maps, cloudCan){
const colorTex = canvasTex(maps.cCan, true);
const specTex = canvasTex(maps.sCan, false);
const bumpTex = canvasTex(maps.bCan, false);
const cloudTex = canvasTex(cloudCan, true);
const globe = new THREE.Mesh(
new THREE.SphereGeometry(1, 128, 80),
new THREE.MeshPhongMaterial({
map:colorTex, specularMap:specTex,
specular:new THREE.Color(0x9fc0cc), shininess:18,
bumpMap:bumpTex, bumpScale:0.45
})
);
planet.add(globe);
cloudsMesh = new THREE.Mesh(
new THREE.SphereGeometry(1.012, 96, 64),
new THREE.MeshLambertMaterial({ map:cloudTex, transparent:true, depthWrite:false })
);
cloudsMesh.renderOrder = 1;
planet.add(cloudsMesh);
const halo = new THREE.Mesh(new THREE.SphereGeometry(1.165, 96, 64), atmoMaterial(0x9ecbdd, 1.0, 3.4, true));
const limb = new THREE.Mesh(new THREE.SphereGeometry(1.008, 96, 64), atmoMaterial(0xbcd6e4, 0.5, 4.0, false));
halo.renderOrder = 3; limb.renderOrder = 2;
tiltGroup.add(halo, limb);
const pos = new Float32Array(RING_N*3), col = new Float32Array(RING_N*3);
const siz = new Float32Array(RING_N), pha = new Float32Array(RING_N);
const c = new THREE.Color();
for(let i = 0; i < RING_N; i++){
const r = 1.52 + Math.pow(Math.random(), 0.62) * 0.95;
rRadius[i] = r;
rAngle[i] = Math.random() * Math.PI * 2;
rY[i] = (Math.random()+Math.random()+Math.random()-1.5) * 0.028 * (0.4 + r - 1.5);
rSpeed[i] = 0.16 * Math.pow(1.9 / r, 1.5) + 0.02;
let s = 0.5 + Math.pow(Math.random(),2) * 2.6;
if(Math.random() < 0.03) s *= 2.2;
siz[i] = s; pha[i] = Math.random() * 6.2832;
if(Math.random() < 0.07) c.setHSL(0.55 + Math.random()*0.05, 0.35, 0.72);
else c.setHSL(0.095 + Math.random()*0.035, 0.6, 0.55 + Math.random()*0.22);
col[i*3]=c.r; col[i*3+1]=c.g; col[i*3+2]=c.b;
}
ringGeo = new THREE.BufferGeometry();
ringGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
ringGeo.setAttribute('aColor', new THREE.BufferAttribute(col, 3));
ringGeo.setAttribute('aSize', new THREE.BufferAttribute(siz, 1));
ringGeo.setAttribute('aPhase', new THREE.BufferAttribute(pha, 1));
ringMat = pointsMaterial(7.0, 0.35, 1.6);
const ring = new THREE.Points(ringGeo, ringMat);
const ringGroup = new THREE.Group();
ringGroup.rotation.set(0.42, 0, -0.16);
ringGroup.add(ring);
tiltGroup.add(ringGroup);
const SN = 1100;
const sp = new Float32Array(SN*3), sc = new Float32Array(SN*3),
ss = new Float32Array(SN), sph = new Float32Array(SN);
const v = new THREE.Vector3();
for(let i = 0; i < SN; i++){
v.set(Math.random()*2-1, Math.random()*2-1, Math.random()*2-1).normalize().multiplyScalar(40 + Math.random()*50);
sp[i*3]=v.x; sp[i*3+1]=v.y; sp[i*3+2]=v.z;
ss[i] = 0.5 + Math.pow(Math.random(),2)*1.4;
sph[i] = Math.random()*6.2832;
const w = Math.random();
if(w < 0.12) c.setHSL(0.10, 0.5, 0.72);
else if(w < 0.24) c.setHSL(0.58, 0.35, 0.74);
else c.setHSL(0, 0, 0.5 + Math.random()*0.35);
sc[i*3]=c.r; sc[i*3+1]=c.g; sc[i*3+2]=c.b;
}
const sg = new THREE.BufferGeometry();
sg.setAttribute('position', new THREE.BufferAttribute(sp, 3));
sg.setAttribute('aColor', new THREE.BufferAttribute(sc, 3));
sg.setAttribute('aSize', new THREE.BufferAttribute(ss, 1));
sg.setAttribute('aPhase', new THREE.BufferAttribute(sph, 1));
starMat = pointsMaterial(90, 0.5, 0.8);
scene.add(new THREE.Points(sg, starMat));
built = true;
}
let baseSpeed = 0, spinVel = 0, dragging = false;
let tiltCur = 0, tiltTgt = 0;
const el = renderer.domElement;
el.style.touchAction = 'none';
const ptrs = new Map();
let lastX = 0, lastY = 0, lastT = 0, pinchD = 0;
const pinchDist = () => {
const a = [...ptrs.values()];
return Math.hypot(a[0].x - a[1].x, a[0].y - a[1].y);
};
el.addEventListener('pointerdown', e => {
el.setPointerCapture(e.pointerId);
ptrs.set(e.pointerId, { x:e.clientX, y:e.clientY });
if(ptrs.size === 1){
dragging = true; spinVel = 0;
lastX = e.clientX; lastY = e.clientY; lastT = performance.now();
} else if(ptrs.size === 2){
dragging = false; pinchD = pinchDist();
}
});
el.addEventListener('pointermove', e => {
par.tx = (e.clientX / innerWidth - 0.5) * 0.16;
par.ty = -(e.clientY / innerHeight - 0.5) * 0.10;
if(!ptrs.has(e.pointerId)) return;
const p = ptrs.get(e.pointerId);
p.x = e.clientX; p.y = e.clientY;
if(ptrs.size === 2){
const nd = pinchDist();
if(pinchD > 0) targetDist = THREE.MathUtils.clamp(targetDist * pinchD / nd, 1.9, 8);
pinchD = nd;
return;
}
if(!dragging) return;
const now = performance.now();
const dx = e.clientX - lastX, dy = e.clientY - lastY;
const dtm = Math.max(now - lastT, 1) / 1000;
planet.rotation.y += dx * 0.0042;
spinVel = spinVel * 0.55 + THREE.MathUtils.clamp(dx * 0.0042 / dtm, -8, 8) * 0.45;
tiltTgt = THREE.MathUtils.clamp(tiltTgt + dy * 0.0028, -1.25, 1.25);
lastX = e.clientX; lastY = e.clientY; lastT = now;
});
const endPtr = e => {
ptrs.delete(e.pointerId);
if(ptrs.size === 0) dragging = false;
else if(ptrs.size === 1){
const p = [...ptrs.values()][0];
dragging = true; lastX = p.x; lastY = p.y; lastT = performance.now();
}
};
el.addEventListener('pointerup', endPtr);
el.addEventListener('pointercancel', endPtr);
el.addEventListener('wheel', e => {
e.preventDefault();
targetDist = THREE.MathUtils.clamp(targetDist * Math.exp(e.deltaY * 0.0011), 1.9, 8);
}, { passive:false });
el.addEventListener('dblclick', () => { tiltTgt = 0; targetDist = 3.4; spinVel = 0; });
const slider = document.getElementById('speed');
const speedVal = document.getElementById('speedVal');
function applySlider(){
const v = +slider.value;
baseSpeed = Math.pow(v / 100, 1.55) * 1.5;
speedVal.textContent = (baseSpeed * 57.29578).toFixed(1) + ' °/s';
slider.style.setProperty('--fill', v + '%');
}
slider.addEventListener('input', applySlider);
applySlider();
const clock = new THREE.Clock();
const roSpin = document.getElementById('roSpin'),
roDist = document.getElementById('roDist'),
roTilt = document.getElementById('roTilt');
let prevRotY = 0, frame = 0;
function tick(){
requestAnimationFrame(tick);
if(!built) return;
const dt = Math.min(clock.getDelta(), 0.05);
const t = clock.elapsedTime;
if(!dragging){
spinVel *= Math.exp(-dt * 0.85);
if(Math.abs(spinVel) < 0.0004) spinVel = 0;
planet.rotation.y += (baseSpeed + spinVel) * dt;
cloudsMesh.rotation.y += (baseSpeed + spinVel) * dt * 0.14;
}
tiltCur += (tiltTgt - tiltCur) * (1 - Math.exp(-dt * 7));
tiltGroup.rotation.x = tiltCur;
const pa = ringGeo.attributes.position.array;
for(let i = 0; i < RING_N; i++){
const a = rAngle[i] + t * rSpeed[i];
pa[i*3] = rRadius[i] * Math.cos(a);
pa[i*3+1] = rY[i];
pa[i*3+2] = rRadius[i] * Math.sin(a);
}
ringGeo.attributes.position.needsUpdate = true;
ringMat.uniforms.uTime.value = t;
starMat.uniforms.uTime.value = t;
dist += (targetDist - dist) * (1 - Math.exp(-dt * 5));
par.x += (par.tx - par.x) * (1 - Math.exp(-dt * 3));
par.y += (par.ty - par.y) * (1 - Math.exp(-dt * 3));
camera.position.set(CAM_DIR.x*dist + par.x, CAM_DIR.y*dist + par.y, CAM_DIR.z*dist);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
if(++frame % 5 === 0){
const spinNow = (planet.rotation.y - prevRotY) / Math.max(dt, 1e-4);
prevRotY = planet.rotation.y;
roSpin.textContent = (spinNow * 57.29578).toFixed(1);
roDist.textContent = dist.toFixed(2) + ' R';
roTilt.textContent = Math.round(tiltCur * 57.29578) + '°';
}
}
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
const pr = Math.min(devicePixelRatio, 2);
renderer.setPixelRatio(pr);
if(ringMat){ ringMat.uniforms.uPR.value = pr; }
if(starMat){ starMat.uniforms.uPR.value = pr; }
});
const veil = document.getElementById('veil'),
vbar = document.querySelector('#vbar i'),
vpct = document.getElementById('vpct');
const setProgress = p => {
vbar.style.width = (p*100).toFixed(1) + '%';
vpct.textContent = Math.round(p*100) + '%';
};
(async () => {
setProgress(0.02);
const maps = await generateMaps(p => setProgress(p * 0.78));
const cloudCan = await generateClouds(p => setProgress(0.78 + p * 0.22));
buildScene(maps, cloudCan);
requestAnimationFrame(tick);
veil.classList.add('done');
setTimeout(() => veil.remove(), 900);
})();
</script>
</body>
</html>
Test 3: 3D Low-Poly Wizard Adventure Game
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skyshard Isles</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Nunito+Sans:wght@400;600;800&display=swap" rel="stylesheet">
<style>
:root{
--ink:#10142a; --line:rgba(255,240,210,.18); --cream:#f6f1e3;
--gold:#e9bd63; --aqua:#7ef0d4; --danger:#ff6b81;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden;background:#161b38}
body{font-family:'Nunito Sans',sans-serif;color:var(--cream)}
canvas{display:block}
/* ---------- HUD ---------- */
#hud{position:fixed;inset:0;pointer-events:none;z-index:5}
#hearts{position:absolute;top:18px;left:20px;display:flex;gap:5px;filter:drop-shadow(0 2px 3px rgba(8,10,25,.55))}
.heart{transition:transform .25s}
.heart svg path{fill:#ff6b81;stroke:#5e1626;stroke-width:1.6;transition:fill .25s,stroke .25s}
.heart.lost{transform:scale(.82)}
.heart.lost svg path{fill:rgba(255,255,255,.06);stroke:rgba(255,255,255,.25)}
#rightCluster{position:absolute;top:18px;right:20px;display:flex;flex-direction:column;align-items:flex-end;gap:9px}
.chip{background:rgba(12,16,34,.55);border:1px solid var(--line);backdrop-filter:blur(6px);
padding:7px 13px;border-radius:11px;display:flex;gap:9px;align-items:center}
#gemChip{font-weight:800;letter-spacing:.06em;font-size:15px}
#gemChip.pop{animation:gpop .35s ease}
@keyframes gpop{40%{transform:scale(1.18)}}
#row{display:flex;gap:9px;align-items:center}
#muteBtn{pointer-events:auto;cursor:pointer;color:var(--cream);padding:7px 9px}
#muteBtn:hover{border-color:var(--gold)}
#muteBtn svg{display:block}
#dial{position:relative;width:44px;height:44px;border-radius:50%;border:1px solid var(--line);
background:rgba(10,14,30,.55);flex:none}
#dialRot{position:absolute;inset:0}
.dsun{position:absolute;left:50%;top:3px;width:9px;height:9px;margin-left:-4.5px;border-radius:50%;background:#ffd98a}
.dmoon{position:absolute;left:50%;bottom:3px;width:7px;height:7px;margin-left:-3.5px;border-radius:50%;background:#cdd6f4}
.dhz{position:absolute;left:6px;right:6px;top:50%;height:1px;background:var(--line)}
#hint{position:absolute;bottom:22px;left:50%;transform:translateX(-50%);
font-size:13px;letter-spacing:.03em;transition:opacity 1.2s;white-space:nowrap}
#hint.gone{opacity:0}
kbd{font-family:inherit;font-size:11.5px;font-weight:800;background:rgba(255,255,255,.09);
border:1px solid rgba(255,255,255,.28);border-bottom-width:2px;border-radius:6px;
padding:2px 7px;margin:0 2px}
#floatLayer{position:absolute;inset:0;overflow:hidden}
.floater{position:absolute;font-family:'MedievalSharp',serif;font-size:22px;color:#8ff5dd;
text-shadow:0 2px 0 rgba(10,20,30,.65);transform:translate(-50%,-50%);
animation:flo .95s ease-out forwards;pointer-events:none}
.floater.bad{color:#ff8d9d}
@keyframes flo{0%{opacity:0;margin-top:8px}15%{opacity:1}100%{opacity:0;margin-top:-52px}}
#vigFlash,#vigLow{position:absolute;inset:0;pointer-events:none;opacity:0;
background:radial-gradient(ellipse at center,transparent 52%,rgba(255,58,58,.42) 100%)}
#vigFlash.flash{animation:vfl .55s ease-out}
@keyframes vfl{0%{opacity:1}100%{opacity:0}}
#vigLow.low{animation:vlo 1.5s ease-in-out infinite}
@keyframes vlo{0%,100%{opacity:.10}50%{opacity:.38}}
/* ---------- Overlays ---------- */
.overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;
opacity:0;pointer-events:none;transition:opacity .4s}
.overlay.show{opacity:1;pointer-events:auto}
.panel{position:relative;max-width:540px;width:min(92vw,540px);text-align:center;
background:rgba(15,19,40,.88);border:1px solid rgba(233,189,99,.42);
backdrop-filter:blur(9px);padding:38px 44px 34px;box-shadow:0 24px 70px rgba(5,8,20,.6)}
.panel::before,.panel::after{content:"";position:absolute;width:9px;height:9px;background:var(--gold);transform:rotate(45deg)}
.panel::before{top:11px;left:11px}
.panel::after{bottom:11px;right:11px}
h1{font-family:'MedievalSharp',serif;font-weight:400;font-size:clamp(30px,5vw,44px);
letter-spacing:.10em;color:var(--cream);text-shadow:0 3px 0 rgba(5,8,20,.55)}
.tag{margin:14px auto 4px;max-width:400px;font-size:14.5px;line-height:1.55;color:rgba(246,241,227,.82)}
.orn{display:flex;align-items:center;justify-content:center;gap:10px;margin:18px 0}
.orn i{height:1px;width:74px;background:linear-gradient(90deg,transparent,var(--gold));display:block}
.orn i:last-child{transform:scaleX(-1)}
.orn b{width:7px;height:7px;background:var(--gold);transform:rotate(45deg);display:block}
.ctrs{display:grid;grid-template-columns:auto auto;gap:9px 22px;justify-content:center;
margin:4px 0 22px;font-size:13.5px}
.ctrs span:nth-child(odd){text-align:right;color:rgba(246,241,227,.72);align-self:center}
.ctrs span:nth-child(even){text-align:left}
.btn{pointer-events:auto;cursor:pointer;font-family:'MedievalSharp',serif;font-size:19px;letter-spacing:.08em;
color:#241a08;background:var(--gold);border:none;padding:13px 40px;
box-shadow:4px 4px 0 rgba(5,8,20,.55);transition:transform .12s,box-shadow .12s}
.btn:hover{transform:translate(-2px,-2px);box-shadow:6px 6px 0 rgba(5,8,20,.55)}
.btn:active{transform:translate(2px,2px);box-shadow:1px 1px 0 rgba(5,8,20,.55)}
.foot{margin-top:16px;font-size:11.5px;letter-spacing:.08em;color:rgba(246,241,227,.45)}
.stats{font-size:15px;letter-spacing:.05em;color:var(--aqua);margin:6px 0 22px}
#deadMsg{color:rgba(246,241,227,.8)}
</style>
</head>
<body>
<div id="hud">
<div id="hearts"></div>
<div id="rightCluster">
<div class="chip" id="gemChip">
<svg viewBox="0 0 24 24" width="19" height="19"><path d="M12 2 2 9.5 12 22 22 9.5Z" fill="#7ef0d4" stroke="#0e5c4a" stroke-width="1.4"/><path d="M2 9.5h20M12 2 7.5 9.5 12 22l4.5-12.5Z" fill="none" stroke="#0e5c4a" stroke-width="1"/></svg>
<span id="gemText">0 / 12</span>
</div>
<div id="row">
<div id="dial"><div id="dialRot"><i class="dsun"></i><i class="dmoon"></i></div><i class="dhz"></i></div>
<div class="chip" id="muteBtn" title="Toggle sound (M)"></div>
</div>
</div>
<div id="hint"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> move <kbd>Space</kbd> jump <kbd>Drag</kbd> look <kbd>Scroll</kbd> zoom</div>
<div id="floatLayer"></div>
<div id="vigLow"></div>
<div id="vigFlash"></div>
</div>
<div class="overlay show" id="ovStart">
<div class="panel">
<h1>SKYSHARD ISLES</h1>
<div class="orn"><i></i><b></b><i></i></div>
<p class="tag">Twelve shards lie scattered across the floating isles.<br>
Gather them all — and mind the elemental wardens.</p>
<div class="orn"><i></i><b></b><i></i></div>
<div class="ctrs">
<span>Move</span><span><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></span>
<span>Jump</span><span><kbd>Space</kbd></span>
<span>Camera</span><span><kbd>Mouse drag</kbd></span>
<span>Zoom</span><span><kbd>Scroll</kbd></span>
<span>Mute</span><span><kbd>M</kbd></span>
</div>
<button class="btn" id="btnStart">BEGIN</button>
<p class="foot">BEST WITH A KEYBOARD · SOUND ON</p>
</div>
</div>
<div class="overlay" id="ovDead">
<div class="panel">
<h1>THE ISLES CLAIM YOU</h1>
<div class="orn"><i></i><b></b><i></i></div>
<p class="tag" id="deadMsg"></p>
<p class="stats" id="deadStats"></p>
<button class="btn" id="btnRetry">RISE AGAIN</button>
</div>
</div>
<div class="overlay" id="ovWin">
<div class="panel">
<h1>ALL SHARDS RECOVERED</h1>
<div class="orn"><i></i><b></b><i></i></div>
<p class="tag">The archipelago hums with restored light.<br>The wardens bow as you pass.</p>
<p class="stats" id="winStats"></p>
<button class="btn" id="btnAgain">WANDER AGAIN</button>
</div>
</div>
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
const lerp=(a,b,t)=>a+(b-a)*t;
const wrapAngle=a=>Math.atan2(Math.sin(a),Math.cos(a));
const sst=(a,b,x)=>{x=clamp((x-a)/(b-a),0,1);return x*x*(3-2*x)};
function mulberry32(s){return function(){s|=0;s=s+0x6D2B79F5|0;let t=Math.imul(s^s>>>15,1|s);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
const rng=mulberry32(20240613);
const fmt=t=>`${Math.floor(t/60)}:${String(Math.floor(t%60)).padStart(2,'0')}`;
const _v1=new THREE.Vector3(),_v2=new THREE.Vector3(),
_v3=new THREE.Vector3(),_v4=new THREE.Vector3(),
_bv=new THREE.Vector3();
let AC=null,master=null,noiseBuf=null,muted=false;
function initAudio(){
if(AC)return;
AC=new (window.AudioContext||window.webkitAudioContext)();
master=AC.createGain(); master.gain.value=muted?0:0.5;
const comp=AC.createDynamicsCompressor();
master.connect(comp); comp.connect(AC.destination);
noiseBuf=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);
const d=noiseBuf.getChannelData(0);
for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;
const src=AC.createBufferSource(); src.buffer=noiseBuf; src.loop=true;
const lp=AC.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=340;
const wg=AC.createGain(); wg.gain.value=0.045;
const lfo=AC.createOscillator(); lfo.frequency.value=0.07;
const lg=AC.createGain(); lg.gain.value=0.022;
lfo.connect(lg); lg.connect(wg.gain);
src.connect(lp); lp.connect(wg); wg.connect(master);
src.start(); lfo.start();
}
function tone(o){
if(!AC)return;
const t0=AC.currentTime+(o.delay||0);
const osc=AC.createOscillator(),g=AC.createGain();
osc.type=o.type||'sine';
osc.frequency.setValueAtTime(o.f0,t0);
osc.frequency.exponentialRampToValueAtTime(Math.max(20,o.f1||o.f0),t0+o.dur);
g.gain.setValueAtTime(0.0001,t0);
g.gain.exponentialRampToValueAtTime(o.vol,t0+0.012);
g.gain.exponentialRampToValueAtTime(0.0001,t0+o.dur);
osc.connect(g); g.connect(master);
osc.start(t0); osc.stop(t0+o.dur+0.05);
}
function noiseHit(o){
if(!AC)return;
const t0=AC.currentTime+(o.delay||0);
const s=AC.createBufferSource(); s.buffer=noiseBuf;
const f=AC.createBiquadFilter(); f.type=o.type||'lowpass';
f.frequency.setValueAtTime(o.f0,t0);
f.frequency.exponentialRampToValueAtTime(Math.max(30,o.f1||o.f0),t0+o.dur);
const g=AC.createGain();
g.gain.setValueAtTime(0.0001,t0);
g.gain.exponentialRampToValueAtTime(o.vol,t0+0.01);
g.gain.exponentialRampToValueAtTime(0.0001,t0+o.dur);
s.connect(f); f.connect(g); g.connect(master);
s.start(t0); s.stop(t0+o.dur+0.05);
}
const sfx={
jump(){tone({f0:330,f1:660,dur:.16,type:'triangle',vol:.16})},
land(hard){noiseHit({dur:.12,vol:hard?.3:.16,f0:520,f1:110});tone({f0:130,f1:70,dur:.1,vol:hard?.2:.1})},
step(){noiseHit({dur:.05,vol:.045,f0:750+Math.random()*250,f1:300})},
collect(n){const b=523*Math.pow(2,((n-1)%12)/12);
tone({f0:b,dur:.14,vol:.2});
tone({f0:b*1.5,dur:.16,vol:.17,delay:.07});
tone({f0:b*2,dur:.2,type:'triangle',vol:.13,delay:.14});
tone({f0:b*3,dur:.3,vol:.05,delay:.14});},
hurt(){tone({f0:270,f1:90,dur:.28,type:'sawtooth',vol:.2});noiseHit({dur:.16,vol:.2,f0:900,f1:150})},
shoot(){tone({f0:760,f1:190,dur:.17,type:'square',vol:.09});noiseHit({dur:.08,vol:.07,f0:2200,f1:500,type:'bandpass'})},
charge(){tone({f0:170,f1:780,dur:.85,vol:.055})},
pop(){noiseHit({dur:.09,vol:.1,f0:900,f1:220})},
whoosh(){noiseHit({dur:.4,vol:.2,f0:280,f1:1400,type:'bandpass'})},
over(){[392,311,262].forEach((f,i)=>tone({f0:f,f1:f*.985,dur:.4,type:'triangle',vol:.18,delay:i*.34}));
tone({f0:98,f1:60,dur:1.3,type:'sine',vol:.14,delay:.1})},
win(){[523,659,784,1047,1319].forEach((f,i)=>tone({f0:f,dur:.3,type:'triangle',vol:.16,delay:i*.11}));
noiseHit({dur:.9,vol:.05,f0:6500,f1:3000,type:'highpass',delay:.2})},
};
const SND_ON='<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10v4h4l5 4V6L7 10H3z" fill="currentColor" stroke="none"/><path d="M16 8.6a4.2 4.2 0 0 1 0 6.8M18.6 6a8.4 8.4 0 0 1 0 12"/></svg>';
const SND_OFF='<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 10v4h4l5 4V6L7 10H3z" fill="currentColor" stroke="none"/><path d="M16.5 9.5l5 5M21.5 9.5l-5 5"/></svg>';
const renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:'high-performance'});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.06;
document.body.appendChild(renderer.domElement);
const cv=renderer.domElement;
cv.addEventListener('contextmenu',e=>e.preventDefault());
const scene=new THREE.Scene();
scene.background=new THREE.Color('#9fe4f2');
scene.fog=new THREE.Fog(new THREE.Color('#dff6f9'),55,175);
const camera=new THREE.PerspectiveCamera(58,innerWidth/innerHeight,0.1,500);
camera.position.set(0,10,30);
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
const glowTex=(()=>{
const c=document.createElement('canvas'); c.width=c.height=64;
const x=c.getContext('2d');
const g=x.createRadialGradient(32,32,0,32,32,32);
g.addColorStop(0,'rgba(255,255,255,1)');
g.addColorStop(0.35,'rgba(255,255,255,.55)');
g.addColorStop(1,'rgba(255,255,255,0)');
x.fillStyle=g; x.fillRect(0,0,64,64);
const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; return t;
})();
const hemi=new THREE.HemisphereLight('#cfeaf5','#c9a98c',0.9); scene.add(hemi);
const amb=new THREE.AmbientLight('#ffffff',0.35); scene.add(amb);
const keyLight=new THREE.DirectionalLight('#fff2d0',1.4);
keyLight.castShadow=true;
keyLight.shadow.mapSize.set(2048,2048);
Object.assign(keyLight.shadow.camera,{left:-36,right:36,top:36,bottom:-36,near:5,far:180});
keyLight.shadow.camera.updateProjectionMatrix();
keyLight.shadow.bias=-0.0004;
scene.add(keyLight); scene.add(keyLight.target);
const SKY_STOPS=[
{t:0.00,sky:'#161b38',hor:'#2a3157',sun:'#aabaff',si:0.00,mi:0.32,hemi:0.24,amb:0.17},
{t:0.20,sky:'#1d2344',hor:'#3a3f6e',sun:'#aabaff',si:0.00,mi:0.32,hemi:0.26,amb:0.18},
{t:0.26,sky:'#7a6a9e',hor:'#ffb08a',sun:'#ffc9a0',si:0.55,mi:0.05,hemi:0.44,amb:0.27},
{t:0.33,sky:'#a9e7f2',hor:'#dff6f9',sun:'#fff2d0',si:1.35,mi:0.00,hemi:0.85,amb:0.38},
{t:0.50,sky:'#9fe4f2',hor:'#e2f7fb',sun:'#fff6dd',si:1.50,mi:0.00,hemi:0.95,amb:0.42},
{t:0.67,sky:'#a9e7f2',hor:'#e8f6f2',sun:'#ffe9bd',si:1.25,mi:0.00,hemi:0.80,amb:0.36},
{t:0.74,sky:'#8a7bb0',hor:'#ff9d7e',sun:'#ffb084',si:0.60,mi:0.04,hemi:0.46,amb:0.28},
{t:0.80,sky:'#232a52',hor:'#4a4478',sun:'#aabaff',si:0.00,mi:0.30,hemi:0.28,amb:0.19},
{t:1.00,sky:'#161b38',hor:'#2a3157',sun:'#aabaff',si:0.00,mi:0.32,hemi:0.24,amb:0.17},
];
SKY_STOPS.forEach(s=>{s.skyC=new THREE.Color(s.sky);s.horC=new THREE.Color(s.hor);s.sunC=new THREE.Color(s.sun)});
const CYCLE=150;
let dayT=0.32, nightF=0;
const _sky=new THREE.Color(),_hor=new THREE.Color(),_sun=new THREE.Color(),_tmpC=new THREE.Color();
const C_WHITE=new THREE.Color('#ffffff'),C_WARM=new THREE.Color('#ffd98a');
const sunSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:'#ffedb8',transparent:true,fog:false,depthWrite:false}));
sunSprite.scale.setScalar(24); scene.add(sunSprite);
const moonSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:'#e6ecff',transparent:true,fog:false,depthWrite:false}));
moonSprite.scale.setScalar(13); scene.add(moonSprite);
var stars;
{
const n=360,pos=new Float32Array(n*3);
for(let i=0;i<n;i++){
const v=new THREE.Vector3(Math.random()*2-1,Math.random()*0.9+0.08,Math.random()*2-1).normalize().multiplyScalar(185);
pos.set([v.x,v.y,v.z],i*3);
}
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.BufferAttribute(pos,3));
stars=new THREE.Points(g,new THREE.PointsMaterial({map:glowTex,color:'#cfd8ff',size:1.7,sizeAttenuation:false,transparent:true,opacity:0,fog:false,depthWrite:false}));
stars.frustumCulled=false; scene.add(stars);
}
const seaMat=new THREE.MeshBasicMaterial({color:'#dff6f9'});
const sea=new THREE.Mesh(new THREE.CircleGeometry(330,40),seaMat);
sea.rotation.x=-Math.PI/2; sea.position.y=-34; scene.add(sea);
const surfaces=[];
const obstacles=[];
const reserved=[];
const world=new THREE.Group(); scene.add(world);
function jitterGeo(geo,amt,keepTop){
const p=geo.attributes.position,h=geo.parameters.height;
for(let i=0;i<p.count;i++){
const y=p.getY(i);
const top=keepTop&&y>h/2-1e-4;
p.setX(i,p.getX(i)+(rng()-0.5)*amt*2);
p.setZ(i,p.getZ(i)+(rng()-0.5)*amt*2);
if(!top)p.setY(i,y+(rng()-0.5)*amt*2);
}
}
const std=(c,o={})=>new THREE.MeshStandardMaterial({color:c,roughness:0.92,metalness:0,flatShading:true,...o});
const ISLANDS=[
{x:0, z:0, y:0, r:11, grass:'#9fe0ae',rock:'#b08a76'},
{x:0, z:-26,y:1.4, r:7, grass:'#b9e6a0',rock:'#a98fa6'},
{x:22, z:-20,y:2.6, r:5.5, grass:'#a5dfc0',rock:'#c2a184'},
{x:27, z:4, y:0.6, r:6.5, grass:'#ffd6e0',rock:'#b08a76'},
{x:20, z:24, y:2.2, r:5, grass:'#a8e6b8',rock:'#9b8aa0'},
{x:-2, z:28, y:1.0, r:6, grass:'#b9e6a0',rock:'#c2a184'},
{x:-24,z:20, y:3.2, r:5.5, grass:'#ffd6e0',rock:'#a98fa6'},
{x:-28,z:-2, y:0.8, r:6.5, grass:'#a5dfc0',rock:'#b08a76'},
{x:-21,z:-21,y:1.8, r:5, grass:'#a8e6b8',rock:'#9b8aa0'},
{x:40, z:-34,y:4.6, r:3.5, grass:'#ffe9a8',rock:'#c2a184'},
{x:-40,z:32, y:4.2, r:3.5, grass:'#cbb3f0',rock:'#a98fa6'},
];
function addIsland(def){
const g=new THREE.Group();
const thick=def.r*0.16+0.9;
const topG=new THREE.CylinderGeometry(def.r,def.r*0.82,thick,Math.max(9,Math.round(def.r*1.7)),1);
jitterGeo(topG,0.14,true); topG.translate(0,-thick/2,0);
const grass=std(def.grass),rock=std(def.rock),rockD=std(def.rock,{color:new THREE.Color(def.rock).multiplyScalar(0.72)});
const top=new THREE.Mesh(topG,[rock,grass,rockD]);
top.receiveShadow=true; g.add(top);
const uh=def.r*1.35+rng()*2;
const botG=new THREE.ConeGeometry(def.r*0.86,uh,9,3);
jitterGeo(botG,0.5,false); botG.rotateX(Math.PI); botG.translate(0,-thick+0.25-uh/2,0);
g.add(new THREE.Mesh(botG,rockD));
g.position.set(def.x,def.y,def.z);
world.add(g);
surfaces.push({type:'disc',x:def.x,z:def.z,y:def.y,r:def.r});
def.thick=thick;
}
ISLANDS.forEach(addIsland);
const wood=std('#c9a27a'),woodD=std('#a87f57');
function bridgeY(s,t){return s.ay+(s.by-s.ay)*t+Math.sin(Math.PI*t)*s.arch}
function addBridge(A,B){
const dx=B.x-A.x,dz=B.z-A.z,L=Math.hypot(dx,dz),ux=dx/L,uz=dz/L;
const s={ax:A.x+ux*A.r,ay:A.y,az:A.z+uz*A.r,
bx:B.x-ux*B.r,by:B.y,bz:B.z-uz*B.r,
width:2.3,arch:Math.min(1.1,L*0.09),len:0};
s.len=Math.hypot(s.bx-s.ax,s.bz-s.az);
const n=Math.max(4,Math.round(s.len/1.35));
const seg=s.len/n;
const plankG=new THREE.BoxGeometry(2.3,0.26,seg*1.18);
const postG=new THREE.CylinderGeometry(0.06,0.07,0.85,5);
const abx=s.bx-s.ax,abz=s.bz-s.az,il=1/s.len;
const px=-abz*il,pz=abx*il;
for(let i=0;i<n;i++){
const t0=i/n,t1=(i+1)/n,tm=(t0+t1)/2;
const y0=bridgeY(s,t0),y1=bridgeY(s,t1);
const m=new THREE.Mesh(plankG,wood);
m.position.set(s.ax+abx*tm,bridgeY(s,tm),s.az+abz*tm);
m.quaternion.setFromEuler(new THREE.Euler(-Math.atan2(y1-y0,seg),Math.atan2(abx,abz),0,'YXZ'));
m.castShadow=m.receiveShadow=true; world.add(m);
if(i%2===0){
const t=t0,y=bridgeY(s,t)+0.34;
for(const side of[-1,1]){
const p=new THREE.Mesh(postG,woodD);
p.position.set(s.ax+abx*t+px*side*(s.width/2-0.14),y,s.az+abz*t+pz*side*(s.width/2-0.14));
p.castShadow=true; world.add(p);
}
}
}
surfaces.push(s);
reserved.push({x:s.ax,z:s.az},{x:s.bx,z:s.bz});
}
addBridge(ISLANDS[0],ISLANDS[1]);
addBridge(ISLANDS[0],ISLANDS[5]);
addBridge(ISLANDS[0],ISLANDS[3]);
function addStones(A,B){
const dx=B.x-A.x,dz=B.z-A.z,D=Math.hypot(dx,dz),ux=dx/D,uz=dz/D;
const px=-uz,pz=ux;
const ax=A.x+ux*A.r,az=A.z+uz*A.r,bx=B.x-ux*B.r,bz=B.z-uz*B.r;
const span=Math.hypot(bx-ax,bz-az);
const n=Math.max(1,Math.round(span/3.6));
for(let i=1;i<=n;i++){
const t=i/(n+1);
const wob=Math.sin(t*Math.PI)*(rng()*1.4-0.7);
const x=ax+(bx-ax)*t+px*wob, z=az+(bz-az)*t+pz*wob;
const y=lerp(A.y,B.y,t)+Math.sin(t*Math.PI)*0.9;
const r=1.35+rng()*0.5;
const g=new THREE.CylinderGeometry(r,r*0.7,0.8,7);
jitterGeo(g,0.1,true); g.translate(0,-0.4,0);
const m=new THREE.Mesh(g,[std('#b08a76'),std(A.grass),std('#8a6a5c')]);
m.position.set(x,y,z); m.castShadow=m.receiveShadow=true; world.add(m);
surfaces.push({type:'disc',x,z,y,r:r+0.15});
reserved.push({x,z});
}
}
[[0,7],[0,2],[1,2],[1,8],[7,8],[5,4],[4,6],[6,5],[2,9],[3,4]].forEach(([a,b])=>addStones(ISLANDS[a],ISLANDS[b]));
{
const g=new THREE.CylinderGeometry(1.7,1.9,0.22,10);
const m=new THREE.Mesh(g,std('#d8cdbb'));
m.position.set(0,0.11,5.5); m.receiveShadow=true; world.add(m);
const ring=new THREE.Mesh(new THREE.TorusGeometry(1.25,0.035,5,20),std('#c9b08a'));
ring.rotation.x=Math.PI/2; ring.position.set(0,0.23,5.5); world.add(ring);
surfaces.push({type:'disc',x:0,z:5.5,y:0.22,r:1.85});
reserved.push({x:0,z:5.5});
}
const CRYSTAL_DEFS=[
{i:0,ox:3.4,oz:2.4},{i:0,ox:-4.6,oz:-3.6},{i:1,ox:1.6,oz:1.4},{i:2,ox:0.6,oz:-1},
{i:3,ox:2.2,oz:0.4},{i:4,ox:-1,oz:1},{i:5,ox:-1.6,oz:-1.2},{i:6,ox:1.2,oz:-0.6},
{i:7,ox:-2,oz:0.8},{i:8,ox:0.4,oz:1.2},{i:9,ox:0,oz:0},{i:10,ox:-1.4,oz:1.2},
];
const CRYSTAL_COLORS=['#7ef0d4','#ffa8c5','#ffe08a','#c9a7ff'];
const TOTAL=CRYSTAL_DEFS.length;
const crystals=[];
CRYSTAL_DEFS.forEach((d,idx)=>{
const isl=ISLANDS[d.i];
const col=CRYSTAL_COLORS[idx%4];
const g=new THREE.Group();
const core=new THREE.Mesh(new THREE.OctahedronGeometry(0.4,0),
new THREE.MeshStandardMaterial({color:'#eafffa',emissive:col,emissiveIntensity:1.1,flatShading:true,roughness:0.3}));
core.scale.y=1.6; core.castShadow=true; g.add(core);
const glow=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:col,transparent:true,opacity:0.75,depthWrite:false}));
glow.scale.setScalar(2.3); g.add(glow);
const beam=new THREE.Mesh(new THREE.CylinderGeometry(0.05,0.05,7,6,1,true),
new THREE.MeshBasicMaterial({color:col,transparent:true,opacity:0.15,blending:THREE.AdditiveBlending,depthWrite:false,side:THREE.DoubleSide}));
beam.position.y=2.7; g.add(beam);
const x=isl.x+d.ox,z=isl.z+d.oz,y=isl.y+1.05;
g.position.set(x,y,z); scene.add(g);
reserved.push({x,z});
crystals.push({g,core,beam,baseY:y,col,on:false,ph:idx*1.7});
});
const ELEMENTS=[
{name:'ember',core:'#ff9a5c',dark:'#8a3d1e'},
{name:'frost',core:'#7ee8fa',dark:'#1e5a8a'},
{name:'bloom',core:'#b0f07e',dark:'#3d6e1e'},
{name:'void', core:'#c9a7ff',dark:'#4a2e8a'},
];
const TURRET_DEFS=[
{i:1,x:0,z:-23.2,el:0},{i:3,x:25.5,z:6.2,el:1},{i:6,x:-22.4,z:19.4,el:2},
{i:2,x:20.4,z:-18.6,el:0},{i:10,x:-40,z:32,el:3},
];
const turrets=[];
TURRET_DEFS.forEach((d,idx)=>{
const isl=ISLANDS[d.i],el=ELEMENTS[d.el];
const g=new THREE.Group(); g.position.set(d.x,isl.y,d.z);
const base=new THREE.Mesh(new THREE.ConeGeometry(1.0,1.1,6),std('#9b93a8')); base.position.y=0.55; base.castShadow=true;
const pil=new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.42,0.95,7),std('#b7ada8')); pil.position.y=1.5; pil.castShadow=true;
const head=new THREE.Group(); head.position.y=2.1;
const spin=new THREE.Group(); head.add(spin);
const ring=new THREE.Mesh(new THREE.TorusGeometry(0.5,0.08,6,12),
new THREE.MeshStandardMaterial({color:el.dark,emissive:el.core,emissiveIntensity:0.7,flatShading:true}));
ring.rotation.x=Math.PI/2; spin.add(ring);
const core=new THREE.Mesh(new THREE.IcosahedronGeometry(0.24,0),
new THREE.MeshStandardMaterial({color:el.dark,emissive:el.core,emissiveIntensity:1.2,flatShading:true}));
core.position.y=0.05; head.add(core);
for(let k=0;k<3;k++){
const sp=new THREE.Mesh(new THREE.ConeGeometry(0.09,0.42,5),std('#8a8296'));
const a=k/3*Math.PI*2;
sp.position.set(Math.cos(a)*0.5,-0.05,Math.sin(a)*0.5);
sp.rotation.z=Math.cos(a)*1.2; sp.rotation.x=-Math.sin(a)*1.2;
head.add(sp);
}
g.add(base,pil,head); scene.add(g);
reserved.push({x:d.x,z:d.z});
obstacles.push({x:d.x,z:d.z,r:0.85,h:1.9,y:isl.y});
turrets.push({g,head,spin,core,el,x:d.x,z:d.z,y:isl.y,range:15,
cool:1.5+Math.random()*2,charging:0,ph:idx*2.1});
});
const trunkMat=std('#8a5f45');
const canopyMats=['#7ccf8f','#8fdc9a','#6fc48f'].map(c=>std(c));
const blossomMat=std('#ffb7cd');
const treeSpots=[],rockSpots=[],tuftSpots=[];
for(const isl of ISLANDS){
const nT=Math.min(6,Math.round(isl.r*0.75)),nR=Math.round(isl.r*0.5),nG=Math.round(isl.r*2.1);
const place=min=>{
for(let k=0;k<24;k++){
const a=rng()*Math.PI*2,d=(0.25+rng()*0.72)*isl.r;
const x=isl.x+Math.cos(a)*d,z=isl.z+Math.sin(a)*d;
if(reserved.every(p=>Math.hypot(p.x-x,p.z-z)>min))return{x,z};
}
return null;
};
for(let i=0;i<nT;i++){const p=place(1.9);if(p)treeSpots.push({...p,y:isl.y,blossom:rng()<0.3,s:0.85+rng()*0.5})}
for(let i=0;i<nR;i++){const p=place(1.4);if(p)rockSpots.push({...p,y:isl.y,s:0.28+rng()*0.34})}
for(let i=0;i<nG;i++){const p=place(0.9);if(p)tuftSpots.push({...p,y:isl.y,s:0.7+rng()*0.8,c:rng()})}
}
treeSpots.forEach(t=>{
const g=new THREE.Group();
const trunk=new THREE.Mesh(new THREE.CylinderGeometry(0.13,0.2,1.0,6),trunkMat);
trunk.position.y=0.5; trunk.castShadow=true; g.add(trunk);
if(t.blossom){
for(const[ox,oy,oz,r]of[[0,1.5,0,0.72],[0.4,1.9,0.15,0.5],[-0.35,1.85,-0.2,0.46]]){
const b=new THREE.Mesh(new THREE.IcosahedronGeometry(r,0),blossomMat);
b.position.set(ox,oy,oz); b.castShadow=true; g.add(b);
}
}else{
const m=canopyMats[Math.floor(rng()*3)];
const c1=new THREE.Mesh(new THREE.ConeGeometry(0.78,1.15,7),m); c1.position.y=1.5; c1.castShadow=true;
const c2=new THREE.Mesh(new THREE.ConeGeometry(0.55,0.95,7),m); c2.position.y=2.15; c2.rotation.y=rng(); c2.castShadow=true;
g.add(c1,c2);
}
g.position.set(t.x,t.y-0.05,t.z); g.scale.setScalar(t.s); g.rotation.y=rng()*6.28;
world.add(g);
obstacles.push({x:t.x,z:t.z,r:0.4*t.s,h:2.6*t.s,y:t.y});
});
{
const rockGeo=new THREE.DodecahedronGeometry(1,0);
const im=new THREE.InstancedMesh(rockGeo,std('#b7ada8'),rockSpots.length);
const M=new THREE.Matrix4(),Q=new THREE.Quaternion(),E=new THREE.Euler(),S=new THREE.Vector3();
rockSpots.forEach((r,i)=>{
E.set(rng(),rng()*6.28,rng()); Q.setFromEuler(E);
S.set(r.s,r.s*0.7,r.s);
M.compose(new THREE.Vector3(r.x,r.y+r.s*0.35,r.z),Q,S);
im.setMatrixAt(i,M);
im.setColorAt(i,_tmpC.set(['#b7ada8','#a9a3b5','#c4b49a'][i%3]));
obstacles.push({x:r.x,z:r.z,r:r.s*0.9,h:r.s*0.8,y:r.y});
});
im.castShadow=im.receiveShadow=true; world.add(im);
}
{
const tuftGeo=new THREE.ConeGeometry(0.09,0.36,4); tuftGeo.translate(0,0.18,0);
const im=new THREE.InstancedMesh(tuftGeo,new THREE.MeshStandardMaterial({roughness:1,flatShading:true}),tuftSpots.length);
const M=new THREE.Matrix4(),Q=new THREE.Quaternion();
tuftSpots.forEach((t,i)=>{
Q.setFromAxisAngle(_v1.set(0,1,0),rng()*6.28);
M.compose(new THREE.Vector3(t.x,t.y,t.z),Q,_v2.set(t.s,t.s,t.s));
im.setMatrixAt(i,M);
im.setColorAt(i,_tmpC.set(t.c<0.5?'#7ccf8f':'#a5e08a').offsetHSL(0,(rng()-0.5)*0.1,(rng()-0.5)*0.08));
});
world.add(im);
}
const STEP=0.55,PR=0.42,GRAV=24,JUMPV=9.3,MAXSP=7.2,KILLY=-26;
function surfaceHeightAt(x,z,refY){
let best=-1e9;
for(let i=0;i<surfaces.length;i++){
const s=surfaces[i];
if(s.type==='disc'){
if(Math.hypot(x-s.x,z-s.z)<s.r+0.35&&s.y<=refY+STEP&&s.y>best)best=s.y;
}else{
const abx=s.bx-s.ax,abz=s.bz-s.az,L2=abx*abx+abz*abz;
let t=((x-s.ax)*abx+(z-s.az)*abz)/L2; t=clamp(t,0,1);
const cx=s.ax+abx*t,cz=s.az+abz*t;
if(Math.hypot(x-cx,z-cz)<s.width/2+0.3){
const y=s.ay+(s.by-s.ay)*t+Math.sin(Math.PI*t)*s.arch;
if(y<=refY+STEP&&y>best)best=y;
}
}
}
return best;
}
const wiz={root:new THREE.Group(),mats:[]};
{
const M=(c,o={})=>{const m=std(c,{roughness:0.85,...o});wiz.mats.push(m);return m};
const teal=M('#2e6e68'),tealD=M('#22544f'),gold=M('#e6b54a'),
skin=M('#f3cba2'),beardM=M('#ece7dc'),dark=M('#2b2b33'),woodM=M('#8a5a3b');
const add=(geo,mat,x,y,z,parent=wiz.root)=>{
const m=new THREE.Mesh(geo,mat); m.position.set(x,y,z); m.castShadow=true; parent.add(m); return m;
};
wiz.robe=add(new THREE.ConeGeometry(0.58,1.15,9),teal,0,0.575,0);
const trim=add(new THREE.TorusGeometry(0.5,0.045,6,14),gold,0,0.07,0); trim.rotation.x=Math.PI/2;
const belt=add(new THREE.TorusGeometry(0.33,0.04,6,12),gold,0,0.66,0); belt.rotation.x=Math.PI/2;
add(new THREE.BoxGeometry(0.1,0.1,0.05),gold,0,0.66,0.33);
add(new THREE.SphereGeometry(0.27,8,6),skin,0,1.3,0);
const nose=add(new THREE.ConeGeometry(0.05,0.13,5),skin,0,1.27,0.29); nose.rotation.x=Math.PI/2;
add(new THREE.SphereGeometry(0.033,6,4),dark,-0.1,1.35,0.22);
add(new THREE.SphereGeometry(0.033,6,4),dark,0.1,1.35,0.22);
const beard=add(new THREE.ConeGeometry(0.19,0.5,7),beardM,0,1.13,0.15); beard.rotation.x=Math.PI-0.35;
wiz.head=new THREE.Group(); wiz.head.position.set(0,1.42,0); wiz.root.add(wiz.head);
const hat=new THREE.Group(); wiz.head.add(hat); wiz.hat=hat;
add(new THREE.CylinderGeometry(0.55,0.6,0.08,10),tealD,0,0.05,0,hat);
add(new THREE.ConeGeometry(0.36,0.62,9),tealD,0,0.4,0,hat);
const tip=add(new THREE.ConeGeometry(0.17,0.55,8),tealD,0.07,0.83,0,hat); tip.rotation.z=-0.3;
const band=add(new THREE.TorusGeometry(0.31,0.035,5,12),gold,0,0.14,0,hat); band.rotation.x=Math.PI/2;
add(new THREE.OctahedronGeometry(0.06),gold,0,0.14,0.32,hat);
const mkArm=side=>{
const p=new THREE.Group(); p.position.set(side*0.44,1.08,0); wiz.root.add(p);
const a=add(new THREE.CylinderGeometry(0.07,0.09,0.55,6),teal,0,-0.26,0,p);
a.rotation.z=side*0.28;
add(new THREE.SphereGeometry(0.075,6,5),skin,0,-0.55,0,p);
return p;
};
wiz.armL=mkArm(-1); wiz.armR=mkArm(1);
const staff=new THREE.Group(); staff.position.set(0.6,0,0.2); staff.rotation.z=-0.1; wiz.root.add(staff);
add(new THREE.CylinderGeometry(0.045,0.06,1.55,6),woodM,0,0.775,0,staff);
wiz.orb=add(new THREE.IcosahedronGeometry(0.15,0),
new THREE.MeshStandardMaterial({color:'#bffce8',emissive:'#7ef0d4',emissiveIntensity:1.6,flatShading:true}),
0,1.62,0,staff);
wiz.mats.push(wiz.orb.material);
const og=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,color:'#7ef0d4',transparent:true,opacity:0.7,depthWrite:false}));
og.scale.setScalar(0.95); og.position.y=1.62; staff.add(og);
wiz.orbLight=new THREE.PointLight('#7ef0d4',2.4,7); wiz.orbLight.position.y=1.62; staff.add(wiz.orbLight);
wiz.root.traverse(o=>{if(o.isMesh)o.castShadow=true});
scene.add(wiz.root);
}
const projs=[];
for(let i=0;i<24;i++){
const m=new THREE.Mesh(new THREE.SphereGeometry(0.2,8,6),new THREE.MeshBasicMaterial({color:'#ffffff'}));
const gl=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,transparent:true,opacity:0.9,depthWrite:false}));
gl.scale.setScalar(1.2); m.add(gl); m.visible=false; scene.add(m);
projs.push({m,vel:new THREE.Vector3(),life:0,trail:0});
}
function fireProjectile(t){
const p=projs.find(p=>p.life<=0); if(!p)return;
t.head.getWorldPosition(_v1);
_v2.set(Math.sin(t.head.rotation.y),0,Math.cos(t.head.rotation.y));
_v1.addScaledVector(_v2,0.6);
_v3.copy(player.pos); _v3.y+=1.05;
const dist=_v1.distanceTo(_v3);
_v3.addScaledVector(player.vel,dist/8.5*0.55);
_v3.sub(_v1).normalize();
p.m.position.copy(_v1);
p.vel.copy(_v3).multiplyScalar(8.5);
p.m.material.color.set(t.el.core);
p.life=4; p.trail=0; p.m.visible=true;
sfx.shoot();
}
function updateTurrets(dt){
for(const t of turrets){
t.spin.rotation.y+=dt*(t.charging>0?7:1.2);
const boost=t.charging>0?(1-t.charging/0.85)*0.9:0;
t.core.scale.setScalar(1+Math.sin(perfT*3+t.ph)*0.08+boost*0.7);
t.core.material.emissiveIntensity=1.1+boost*2.2;
if(state!=='playing'||graceT>0){t.head.rotation.y+=dt*0.4;continue}
const dx=player.pos.x-t.x,dz=player.pos.z-t.z;
const dist=Math.hypot(dx,dz,player.pos.y-t.y);
if(dist<t.range){
const want=Math.atan2(dx,dz),diff=wrapAngle(want-t.head.rotation.y);
t.head.rotation.y+=diff*Math.min(1,dt*5);
if(t.cool<=0&&t.charging<=0){t.charging=0.85;sfx.charge()}
if(t.charging>0){
t.charging-=dt;
if(t.charging<=0){
if(Math.abs(diff)<0.4)fireProjectile(t);
else t.charging=0.15;
t.cool=2.3*(0.85+Math.random()*0.3);
}
}
}else{t.head.rotation.y+=dt*0.4;t.charging=0}
t.cool-=dt;
}
}
function updateProjs(dt){
for(const p of projs){
if(p.life<=0)continue;
p.life-=dt;
p.m.position.addScaledVector(p.vel,dt);
p.trail-=dt;
if(p.trail<=0){p.trail=0.045;spawnP(p.m.position,p.m.material.color.getHex(),_v1.set(0,0,0),0.25,0.5,0)}
const dx=p.m.position.x-player.pos.x,dy=p.m.position.y-(player.pos.y+0.95),dz=p.m.position.z-player.pos.z;
let dead=p.life<=0||p.m.position.y<KILLY-6;
if(!dead&&state==='playing'&&dx*dx+dy*dy+dz*dz<0.72){
damage(p.vel,'warden'); dead=true;
}
if(!dead){
const g=surfaceHeightAt(p.m.position.x,p.m.position.z,p.m.position.y+0.5);
if(g>-1e8&&p.m.position.y<g)dead=true;
}
if(dead){
burst(p.m.position,p.m.material.color.getHex(),8,2,0.55,0.35,2);
sfx.pop(); p.life=0; p.m.visible=false;
}
}
}
const POOL=150,bursts=[];let burstIdx=0;
for(let i=0;i<POOL;i++){
const s=new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex,transparent:true,depthWrite:false,blending:THREE.AdditiveBlending}));
s.visible=false; scene.add(s);
bursts.push({s,vel:new THREE.Vector3(),life:0,max:1,size:1,grav:0});
}
function spawnP(pos,color,vel,life,size,grav){
const b=bursts[burstIdx++%POOL];
b.s.visible=true; b.s.position.copy(pos);
b.s.material.color.set(color); b.s.material.opacity=1;
b.vel.copy(vel); b.life=b.max=life; b.size=size; b.grav=grav;
}
function burst(pos,color,n,spd,size,life,grav=3,up=1.6){
for(let i=0;i<n;i++){
_bv.set(Math.random()*2-1,Math.random()*2-1,Math.random()*2-1).normalize()
.multiplyScalar(spd*(0.4+Math.random()*0.6));
_bv.y+=up;
spawnP(pos,color,_bv,life*(0.7+Math.random()*0.5),size,grav);
}
}
function updateBursts(dt){
for(const b of bursts){
if(b.life<=0)continue;
b.life-=dt;
if(b.life<=0){b.s.visible=false;continue}
b.s.position.addScaledVector(b.vel,dt);
b.vel.y-=b.grav*dt;
const k=b.life/b.max;
b.s.material.opacity=k;
b.s.scale.setScalar(b.size*(0.4+0.6*k));
}
}
const motes={n:240};
{
const pos=new Float32Array(motes.n*3);
motes.vel=new Float32Array(motes.n*3);
for(let i=0;i<motes.n;i++){
pos[i*3]=(Math.random()*2-1)*55; pos[i*3+1]=Math.random()*20-3; pos[i*3+2]=(Math.random()*2-1)*55;
motes.vel[i*3]=(Math.random()-0.5)*0.3; motes.vel[i*3+1]=(Math.random()-0.5)*0.1; motes.vel[i*3+2]=(Math.random()-0.5)*0.3;
}
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.BufferAttribute(pos,3));
motes.mat=new THREE.PointsMaterial({map:glowTex,size:0.18,transparent:true,opacity:0.4,depthWrite:false,blending:THREE.AdditiveBlending,color:'#ffffff'});
motes.pts=new THREE.Points(g,motes.mat); motes.pts.frustumCulled=false; scene.add(motes.pts);
motes.geo=g;
}
function updateMotes(dt){
const p=motes.geo.attributes.position.array;
for(let i=0;i<motes.n;i++){
const j=i*3;
p[j]+=(motes.vel[j]+Math.sin(perfT*0.7+i)*0.18)*dt;
p[j+1]+=motes.vel[j+1]*dt;
p[j+2]+=(motes.vel[j+2]+Math.cos(perfT*0.6+i*1.3)*0.18)*dt;
if(p[j]>55)p[j]=-55; if(p[j]<-55)p[j]=55;
if(p[j+1]>20)p[j+1]=-3; if(p[j+1]<-4)p[j+1]=20;
if(p[j+2]>55)p[j+2]=-55; if(p[j+2]<-55)p[j+2]=55;
}
motes.geo.attributes.position.needsUpdate=true;
}
const clouds=[];
for(let i=0;i<7;i++){
const g=new THREE.Group();
const mat=std('#ffffff',{transparent:true,opacity:0.95});
const nP=4+Math.floor(Math.random()*3);
for(let k=0;k<nP;k++){
const s=new THREE.Mesh(new THREE.IcosahedronGeometry(1.1+Math.random()*1.3,0),mat);
s.position.set((Math.random()-0.5)*4.5,(Math.random()-0.5)*0.8,(Math.random()-0.5)*2.2);
s.scale.y=0.6; g.add(s);
}
g.scale.setScalar(1.5+Math.random()*1.2);
scene.add(g);
clouds.push({g,ang:Math.random()*6.28,rad:28+Math.random()*34,h:8+Math.random()*9,spd:(0.008+Math.random()*0.012)*(Math.random()<0.5?-1:1),rot:(Math.random()-0.5)*0.1});
}
function updateClouds(dt){
for(const c of clouds){
c.ang+=c.spd*dt;
c.g.position.set(Math.cos(c.ang)*c.rad,c.h,Math.sin(c.ang)*c.rad);
c.g.rotation.y+=c.rot*dt;
}
}
const drifters=[];
for(let i=0;i<10;i++){
const m=new THREE.Mesh(new THREE.OctahedronGeometry(0.8+Math.random()*1.5,0),
std(['#b08a76','#a98fa6','#c2a184'][i%3]));
scene.add(m);
drifters.push({m,ang:Math.random()*6.28,rad:55+Math.random()*30,h:-6+Math.random()*20,spd:(0.004+Math.random()*0.006)*(Math.random()<0.5?-1:1),rx:(Math.random()-0.5)*0.4,ry:(Math.random()-0.5)*0.4});
}
function updateDrifters(dt){
for(const d of drifters){
d.ang+=d.spd*dt;
d.m.position.set(Math.cos(d.ang)*d.rad,d.h,Math.sin(d.ang)*d.rad);
d.m.rotation.x+=d.rx*dt; d.m.rotation.y+=d.ry*dt;
}
}
const keys={};
let camYaw=0.35,camPitch=0.42,camDist=9.5,dragging=false,lastPX=0,lastPY=0;
const SENS=0.0052,PITCH_MIN=-0.1,PITCH_MAX=1.15;
addEventListener('keydown',e=>{
if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault();
keys[e.code]=true;
if(e.code==='Space'){jumpBuf=0.14;hintJump=true}
if(e.code==='KeyM')toggleMute();
if(e.code==='Enter'){
if(state==='menu'||state==='dead'||state==='win')beginRun();
}
});
addEventListener('keyup',e=>keys[e.code]=false);
addEventListener('blur',()=>{for(const k in keys)keys[k]=false});
cv.addEventListener('pointerdown',e=>{
if(state==='menu')return;
dragging=true; lastPX=e.clientX; lastPY=e.clientY;
cv.setPointerCapture(e.pointerId);
});
cv.addEventListener('pointermove',e=>{
if(!dragging)return;
const dx=e.clientX-lastPX,dy=e.clientY-lastPY;
lastPX=e.clientX; lastPY=e.clientY;
camYaw-=dx*SENS;
camPitch=clamp(camPitch+dy*SENS,PITCH_MIN,PITCH_MAX);
});
const endDrag=()=>dragging=false;
cv.addEventListener('pointerup',endDrag);
cv.addEventListener('pointercancel',endDrag);
cv.addEventListener('wheel',e=>{
e.preventDefault();
camDist=clamp(camDist*Math.exp(e.deltaY*0.0012),5,16);
},{passive:false});
const $=id=>document.getElementById(id);
const heartsEl=$('hearts'),gemText=$('gemText'),gemChip=$('gemChip'),
dialRot=$('dialRot'),hintEl=$('hint'),vigFlash=$('vigFlash'),vigLow=$('vigLow'),
floatLayer=$('floatLayer'),
ovStart=$('ovStart'),ovDead=$('ovDead'),ovWin=$('ovWin'),
deadMsg=$('deadMsg'),deadStats=$('deadStats'),winStats=$('winStats'),
muteBtn=$('muteBtn');
const HEART='<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
const MAXHP=5;
function renderHearts(){
if(!heartsEl)return;
heartsEl.innerHTML='';
for(let i=0;i<MAXHP;i++){
const s=document.createElement('span');
s.className='heart'+(i<hp?'':' lost');
s.innerHTML=HEART;
heartsEl.appendChild(s);
}
if(vigLow)vigLow.classList.toggle('low',hp===1&&state==='playing');
}
function renderGems(){
if(!gemText)return;
gemText.textContent=`${score} / ${TOTAL}`;
if(gemChip){gemChip.classList.remove('pop'); void gemChip.offsetWidth; gemChip.classList.add('pop');}
}
function toggleMute(){
muted=!muted;
if(master)master.gain.value=muted?0:0.5;
if(muteBtn)muteBtn.innerHTML=muted?SND_OFF:SND_ON;
}
if(muteBtn){
muteBtn.innerHTML=SND_ON;
muteBtn.addEventListener('click',e=>{initAudio();toggleMute();e.currentTarget.blur()});
}
function floater(worldPos,text,bad){
if(!floatLayer)return;
_v1.copy(worldPos).project(camera);
if(_v1.z>1)return;
const d=document.createElement('div');
d.className='floater'+(bad?' bad':'');
d.textContent=text;
d.style.left=((_v1.x*0.5+0.5)*innerWidth)+'px';
d.style.top=((-_v1.y*0.5+0.5)*innerHeight)+'px';
floatLayer.appendChild(d);
setTimeout(()=>d.remove(),960);
}
function vigPulse(){if(vigFlash){vigFlash.classList.remove('flash');void vigFlash.offsetWidth;vigFlash.classList.add('flash')}}
function showOv(el){[ovStart,ovDead,ovWin].forEach(o=>{if(o)o.classList.toggle('show',o===el)})}
let state='menu',hp=MAXHP,score=0,hits=0,gameTime=0,graceT=0;
let timeScale=1,invuln=0,shake=0,dip=0,dipV=0,perfT=0,T=0;
let lastDamage='warden',hintMoved=false,hintJump=false,hintDone=false;
const player={pos:new THREE.Vector3(0,0.3,5.5),vel:new THREE.Vector3()};
let grounded=false,prevGrounded=false,coyote=0,jumpBuf=0,heading=Math.PI;
let walkPhase=0,stepAcc=0,deform=0,deformV=0,flashT=0;
const lastSafe=new THREE.Vector3(0,0.3,5.5);
const tweens=[];
function tween(dur,fn,onDone){tweens.push({t:0,dur,fn,onDone})}
function updateTweens(dt){
for(let i=tweens.length-1;i>=0;i--){
const tw=tweens[i]; tw.t+=dt;
const k=clamp(tw.t/tw.dur,0,1);
tw.fn(k*k*(3-2*k));
if(k>=1){tweens.splice(i,1);tw.onDone&&tw.onDone()}
}
}
function beginRun(){
initAudio(); if(AC&&AC.state==='suspended')AC.resume();
hp=MAXHP;score=0;hits=0;gameTime=0;graceT=2.4;timeScale=1;invuln=1.5;
shake=0;dip=0;dipV=0;deform=0;deformV=0;flashT=0;
player.pos.set(0,0.3,5.5); player.vel.set(0,0,0);
heading=Math.PI; lastSafe.copy(player.pos);
wiz.root.visible=true; wiz.root.rotation.set(0,Math.PI,0);
crystals.forEach(c=>{c.on=false;c.g.visible=true;c.g.scale.setScalar(1)});
projs.forEach(p=>{p.life=0;p.m.visible=false});
turrets.forEach(t=>{t.cool=1.2+Math.random()*2;t.charging=0});
renderHearts(); if(gemText)gemText.textContent=`0 / ${TOTAL}`;
showOv(null);
state='playing';
if(!hintDone)setTimeout(()=>{if(!hintDone)dismissHint()},25000);
}
const btnStart=$('btnStart'); if(btnStart)btnStart.addEventListener('click',e=>{beginRun();e.currentTarget.blur()});
const btnRetry=$('btnRetry'); if(btnRetry)btnRetry.addEventListener('click',e=>{beginRun();e.currentTarget.blur()});
const btnAgain=$('btnAgain'); if(btnAgain)btnAgain.addEventListener('click',e=>{beginRun();e.currentTarget.blur()});
function dismissHint(){hintDone=true;if(hintEl)hintEl.classList.add('gone')}
function damage(srcVel,cause){
if(invuln>0||state!=='playing')return;
hp--; hits++; lastDamage=cause;
renderHearts(); vigPulse(); sfx.hurt();
shake=0.35; flashT=0.2;
wiz.mats.forEach(m=>{m.emissive.set('#ff5544');m.emissiveIntensity=0.8});
if(srcVel){_v4.copy(srcVel);_v4.y=0;if(_v4.lengthSq()>0.01){_v4.normalize();player.vel.addScaledVector(_v4,5);player.vel.y=3.5}}
floater(_v2.copy(player.pos).add(_v3.set(0,2.2,0)),'-1',true);
if(hp<=0)die();
else invuln=1.3;
}
function die(){
state='dead';
burst(_v1.copy(player.pos).add(_v2.set(0,1,0)),'#7ef0d4',40,5,1.1,0.9,3);
wiz.root.visible=false;
sfx.over();
renderHearts();
setTimeout(()=>{
if(state!=='dead')return;
if(deadMsg)deadMsg.textContent=lastDamage==='fall'
?'You slipped beneath the cloud sea. The isles are patient — try once more.'
:'A warden’s bolt found its mark. Their aim is true, but your feet can be quicker.';
if(deadStats)deadStats.textContent=`Shards ${score} / ${TOTAL} · Time ${fmt(gameTime)}`;
showOv(ovDead);
},1000);
}
function onFall(){
hp--; hits++; lastDamage='fall';
renderHearts(); vigPulse(); sfx.hurt(); sfx.whoosh();
if(hp<=0){die();return}
player.pos.copy(lastSafe); player.pos.y+=0.2;
player.vel.set(0,0,0); invuln=1.2;
}
function collect(c){
c.on=true; score++;
renderGems(); sfx.collect(score);
burst(c.g.position,c.col,18,3.5,0.8,0.6,2.5);
floater(c.g.position,'+1');
const g=c.g,s0=g.scale.x;
tween(0.28,k=>{g.scale.setScalar(s0*(1-k));g.position.y=c.baseY+k*1.2},()=>{g.visible=false});
if(score===TOTAL)triggerWin();
}
function triggerWin(){
timeScale=0.22;
sfx.win();
for(let i=0;i<3;i++){
const col=['#ffe08a','#7ef0d4','#ffa8c5'][i];
tween(0.2+i*0.25,()=>{},()=>burst(_v1.copy(player.pos).add(_v2.set((i-1)*1.5,2.5+i*0.7,(i-1)*-1.2)),col,26,4.5,1,0.9,2.5));
}
setTimeout(()=>{
if(state!=='playing'||score<TOTAL)return;
state='win';
if(winStats)winStats.textContent=`Time ${fmt(gameTime)} · Hits taken ${hits}`;
showOv(ovWin);
},1400);
}
function physics(dt){
const fA=(keys.KeyW||keys.ArrowUp?1:0)-(keys.KeyS||keys.ArrowDown?1:0);
const sA=(keys.KeyD||keys.ArrowRight?1:0)-(keys.KeyA||keys.ArrowLeft?1:0);
const fx=-Math.sin(camYaw),fz=-Math.cos(camYaw),rx=Math.cos(camYaw),rz=-Math.sin(camYaw);
let mx=fx*fA+rx*sA,mz=fz*fA+rz*sA;
const ml=Math.hypot(mx,mz);
if(ml>1){mx/=ml;mz/=ml}
if(ml>0.01)hintMoved=true;
const ctrl=grounded?1:0.65;
player.vel.x+=mx*48*ctrl*dt;
player.vel.z+=mz*48*ctrl*dt;
const hs=Math.hypot(player.vel.x,player.vel.z);
if(ml<=0.01&&grounded){
const d=Math.exp(-9.5*dt);
player.vel.x*=d; player.vel.z*=d;
}else if(hs>MAXSP){
player.vel.x*=MAXSP/hs; player.vel.z*=MAXSP/hs;
}
player.pos.x+=player.vel.x*dt;
player.pos.z+=player.vel.z*dt;
for(const o of obstacles){
if(player.pos.y>o.y+o.h)continue;
const dx=player.pos.x-o.x,dz=player.pos.z-o.z;
const d=Math.hypot(dx,dz),min=o.r+PR;
if(d<min&&d>1e-4){
player.pos.x=o.x+dx/d*min;
player.pos.z=o.z+dz/d*min;
}
}
const prevY=player.pos.y;
player.vel.y-=GRAV*dt;
player.pos.y+=player.vel.y*dt;
const g=surfaceHeightAt(player.pos.x,player.pos.z,prevY+STEP);
prevGrounded=grounded; grounded=false;
if(player.vel.y<=0&&g>-1e8&&player.pos.y<=g&&(prevY>=g-0.001||player.pos.y>g-0.6)){
player.pos.y=g;
if(!prevGrounded){
const hard=player.vel.y<-11;
sfx.land(hard);
dipV-=Math.min(1,Math.abs(player.vel.y)/14)*2.2;
deformV-=Math.min(1,Math.abs(player.vel.y)/14)*3;
burst(_v1.set(player.pos.x,g+0.1,player.pos.z),'#efe6d2',hard?12:6,hard?2.6:1.6,0.5,0.4,3,1);
}
player.vel.y=0; grounded=true; coyote=0.12;
}else{
coyote-=dt;
}
jumpBuf-=dt;
if(jumpBuf>0&&(grounded||coyote>0)){
player.vel.y=JUMPV; grounded=false; coyote=0; jumpBuf=0;
sfx.jump(); deformV+=1.4; hintJump=true;
burst(_v1.set(player.pos.x,player.pos.y+0.1,player.pos.z),'#efe6d2',5,1.4,0.4,0.3,2,0.5);
}
if(player.pos.y<KILLY&&state==='playing')onFall();
if(grounded){
let safe=false;
for(const s of surfaces){
if(s.type==='disc'){
if(Math.hypot(player.pos.x-s.x,player.pos.z-s.z)<s.r-0.8&&Math.abs(player.pos.y-s.y)<0.3){safe=true;break}
}else{
const abx=s.bx-s.ax,abz=s.bz-s.az,L2=abx*abx+abz*abz;
const t=clamp(((player.pos.x-s.ax)*abx+(player.pos.z-s.az)*abz)/L2,0,1);
const cx=s.ax+abx*t,cz=s.az+abz*t;
if(Math.hypot(player.pos.x-cx,player.pos.z-cz)<s.width/2-0.5&&t>0.08&&t<0.92){safe=true;break}
}
}
if(safe)lastSafe.copy(player.pos);
}
if(grounded&&hs>1){
stepAcc+=hs*dt;
if(stepAcc>2.1){stepAcc=0;sfx.step()}
}
if(ml>0.15){
const target=Math.atan2(player.vel.x,player.vel.z);
const dh=wrapAngle(target-heading);
heading+=dh*Math.min(1,dt*10);
wiz.root.rotation.z=lerp(wiz.root.rotation.z,clamp(-dh*0.6,-0.22,0.22),Math.min(1,dt*8));
}else{
wiz.root.rotation.z=lerp(wiz.root.rotation.z,0,Math.min(1,dt*6));
}
wiz.root.rotation.y=heading;
wiz.root.position.copy(player.pos);
invuln-=dt;
if(flashT>0){
flashT-=dt;
if(flashT<=0)wiz.mats.forEach(m=>{m.emissive.set('#000000');m.emissiveIntensity=0});
}
if(invuln>0)wiz.root.visible=Math.floor(perfT*14)%2===0;
else if(state!=='dead')wiz.root.visible=true;
if(state==='playing'){
for(const c of crystals){
if(c.on)continue;
const dx=player.pos.x-c.g.position.x,dy=player.pos.y+0.9-c.g.position.y,dz=player.pos.z-c.g.position.z;
if(dx*dx+dy*dy+dz*dz<2.1)collect(c);
}
}
}
function animateWizard(dt){
const hs=Math.hypot(player.vel.x,player.vel.z),sr=clamp(hs/MAXSP,0,1);
walkPhase+=dt*(4+hs*1.7);
deformV+=(-deform*160-deformV*10)*dt;
deform=clamp(deform+deformV*dt,-0.45,0.5);
wiz.root.scale.set(1-deform*0.35,1+deform,1-deform*0.35);
const bob=grounded?Math.abs(Math.sin(walkPhase))*0.09*sr:0;
wiz.root.position.y=player.pos.y+bob;
wiz.robe.rotation.x=sr*0.09;
wiz.robe.scale.x=wiz.robe.scale.z=1+Math.sin(walkPhase*2)*0.03*sr;
if(!grounded){
wiz.armL.rotation.x=lerp(wiz.armL.rotation.x,-2.5,Math.min(1,dt*8));
wiz.armR.rotation.x=lerp(wiz.armR.rotation.x,-2.1,Math.min(1,dt*8));
}else{
wiz.armL.rotation.x=Math.sin(walkPhase)*0.55*sr+Math.sin(perfT*1.4)*0.06;
wiz.armR.rotation.x=-Math.sin(walkPhase)*0.2*sr+Math.sin(perfT*1.4+1)*0.06;
}
wiz.head.rotation.y=Math.sin(perfT*0.6)*0.25*(1-sr);
wiz.hat.rotation.z=Math.sin(walkPhase)*0.05*sr+Math.sin(perfT*1.1)*0.02;
wiz.orb.position.y=1.62+Math.sin(perfT*2.2)*0.05;
wiz.orb.rotation.y+=dt*2;
wiz.orbLight.intensity=2.2+Math.sin(perfT*3.1)*0.6+nightF*1.3;
if(state==='win'&&grounded){player.vel.y=5.5;grounded=false;heading+=dt*4;wiz.root.rotation.y=heading}
}
function updateCrystals(dt){
let best=-1,bestD=1e9;
for(let i=0;i<crystals.length;i++){
const c=crystals[i];
if(c.on)continue;
const d=c.g.position.distanceToSquared(player.pos);
if(d<bestD){bestD=d;best=i}
}
for(let i=0;i<crystals.length;i++){
const c=crystals[i];
if(c.on)continue;
c.g.position.y=c.baseY+Math.sin(perfT*1.8+c.ph)*0.16;
c.core.rotation.y+=dt*1.4;
c.core.rotation.x=Math.sin(perfT+c.ph)*0.2;
c.beam.material.opacity=lerp(c.beam.material.opacity,i===best?0.42:0.13+nightF*0.08,Math.min(1,dt*5));
}
}
const camPos=new THREE.Vector3(0,10,30),camTarget=new THREE.Vector3();
let camSnap=false,menuAng=0;
function updateCamera(rdt){
if(state==='menu'){
menuAng+=rdt*0.1;
_v1.set(Math.sin(menuAng)*32,9+Math.sin(menuAng*0.6)*2,Math.cos(menuAng)*32);
camPos.lerp(_v1,Math.min(1,rdt*2));
camera.position.copy(camPos);
camera.lookAt(0,2.5,0);
return;
}
camTarget.set(player.pos.x,player.pos.y+1.7,player.pos.z);
const cp=Math.cos(camPitch);
_v1.set(Math.sin(camYaw)*cp,Math.sin(camPitch),Math.cos(camYaw)*cp).multiplyScalar(camDist).add(camTarget);
if(!camSnap){camPos.copy(_v1);camSnap=true}
camPos.lerp(_v1,1-Math.exp(-rdt*7));
const gh=surfaceHeightAt(camPos.x,camPos.z,camPos.y+10);
if(gh>-1e8&&camPos.y<gh+0.6)camPos.y=gh+0.6;
camera.position.copy(camPos);
if(shake>0){
camera.position.x+=(Math.random()-0.5)*shake;
camera.position.y+=(Math.random()-0.5)*shake;
shake*=Math.exp(-rdt*6);
}
dipV+=(-dip*180-dipV*12)*rdt;
dip+=dipV*rdt;
camera.position.y+=dip;
camera.lookAt(camTarget);
}
function checkHint(){
if(hintDone||state!=='playing')return;
if(hintMoved&&hintJump)setTimeout(dismissHint,1200);
}
const clock=new THREE.Clock();
renderer.setAnimationLoop(()=>{
const rdt=Math.min(clock.getDelta(),0.05);
perfT+=rdt;
timeScale+=(1-timeScale)*Math.min(1,rdt*1.6);
const dt=rdt*timeScale;
T+=dt;
if(state==='playing'){
gameTime+=dt; graceT-=dt;
physics(dt); checkHint();
}else if(state==='win'){
physics(dt);
}else if(state==='menu'){
wiz.root.position.set(0,0.05,5.5);
wiz.head.rotation.y=Math.sin(perfT*0.5)*0.4;
}
updateTurrets(dt);
updateProjs(dt);
updateCrystals(dt);
animateWizard(dt);
updateBursts(dt);
updateMotes(dt);
updateClouds(dt);
updateDrifters(dt);
updateTweens(dt);
updateCamera(rdt);
renderer.render(scene,camera);
});
renderHearts();
</script>
</body>
</html>
Final Verdict
Read Next
Gemini 3.7 Flash Released: Specs, Benchmarks & Interactive Coding Tests
Google released Gemini 3.7 Flash on August 13, 2026. With a 1M token context window, $0.75/M input & $3.75/M output pricing, and an Intelligence Index score of 56, we test its SVG origami crane and 3D WebGL wizard game coding performance.
DeepSeek V4 Pro 0813 Released: Specs, Benchmarks & Hands-On Vector/3D Coding Tests
DeepSeek V4 Pro 0813 released on August 12, 2026. With 1.6T total parameters (49B active per token), 1M context, and $0.435/M input & $0.87/M output pricing, we run the first hands-on vector SVG and 3D globe benchmarks.
Grok 4.6 Released: Specs, Benchmarks & Hands-On Vector/3D Coding Tests
xAI's Grok 4.6 frontier model brings a 500k context window, $2/M input, $6/M output pricing, and an Artificial Analysis Intelligence score of 61. We test its SVG vector art capabilities and interactive Three.js 3D WebGL globe simulation.