Created
June 14, 2026 15:52
-
-
Save sshh12/84fbc14911652cfdf4c9b1297dda1d18 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PlanetLab — Procedural Planet, Moon & Small-Body Builder</title> | |
| <style> | |
| :root{ | |
| --bg:#0a0c10; --panel:#12161d; --panel2:#1a1f29; --line:#283040; | |
| --txt:#cfd8e6; --dim:#8a97ad; --accent:#ff8a4c; --accent2:#5cc8ff; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{margin:0;height:100%;background:var(--bg);color:var(--txt); | |
| font-family:"Segoe UI",system-ui,sans-serif;font-size:13px;overflow:hidden} | |
| #app{display:flex;height:100vh} | |
| #panel{width:352px;min-width:352px;height:100%;overflow-y:auto; | |
| background:var(--panel);border-right:1px solid var(--line);padding:14px 14px 60px} | |
| #panel h1{font-size:16px;margin:0 0 2px;letter-spacing:.5px} | |
| #panel h1 span{color:var(--accent)} | |
| .sub{color:var(--dim);font-size:11px;margin-bottom:12px;line-height:1.5} | |
| .sec{border:1px solid var(--line);border-radius:8px;margin-bottom:9px;background:var(--panel2)} | |
| .sec>.hd{padding:8px 11px;cursor:pointer;display:flex;justify-content:space-between; | |
| align-items:center;font-weight:600;letter-spacing:.3px;user-select:none;font-size:12.5px} | |
| .sec>.hd .tg{color:var(--dim);font-weight:400} | |
| .sec>.bd{padding:4px 11px 11px;display:none} | |
| .sec.open>.bd{display:block} | |
| .row{display:flex;align-items:center;justify-content:space-between;margin:8px 0;gap:8px} | |
| .row label{color:var(--dim);flex:1;font-size:11.5px} | |
| .row .val{color:var(--accent2);font-variant-numeric:tabular-nums;width:42px;text-align:right;font-size:11px} | |
| input[type=range]{flex:1.3;accent-color:var(--accent);height:4px} | |
| select,input[type=number]{background:#0d1016;color:var(--txt);border:1px solid var(--line); | |
| border-radius:5px;padding:5px 7px;font-size:12px;width:100%} | |
| button{background:#222a36;color:var(--txt);border:1px solid var(--line);border-radius:6px; | |
| padding:8px 10px;cursor:pointer;font-size:12px;font-weight:600;transition:.12s} | |
| button:hover{background:#2d3848;border-color:#3a475c} | |
| button.primary{background:var(--accent);color:#1a1206;border-color:var(--accent)} | |
| button.primary:hover{filter:brightness(1.08)} | |
| button.alt{background:#1c2a38;border-color:#274056;color:var(--accent2)} | |
| .btnrow{display:flex;gap:6px;margin:10px 0} | |
| .btnrow button{flex:1} | |
| #stage{flex:1;position:relative;display:flex;align-items:center;justify-content:center; | |
| background:radial-gradient(circle at 50% 40%,#0d1119,#05060a 70%)} | |
| #view{display:block;cursor:grab;touch-action:none} | |
| #view:active{cursor:grabbing} | |
| #hud{position:absolute;top:14px;left:16px;font-size:11px;color:var(--dim); | |
| pointer-events:none;line-height:1.7;text-shadow:0 1px 3px #000} | |
| #hud b{color:var(--accent)} | |
| #spin{position:absolute;bottom:16px;left:50%;transform:translateX(-50%); | |
| display:flex;gap:8px;align-items:center;background:#0d1119cc;border:1px solid var(--line); | |
| border-radius:20px;padding:6px 14px;font-size:11px;color:var(--dim);backdrop-filter:blur(4px)} | |
| #spin button{padding:4px 10px;border-radius:14px} | |
| #codexBtn{position:absolute;top:14px;right:16px} | |
| #codex{position:absolute;inset:0;background:#070a0fee;backdrop-filter:blur(6px); | |
| overflow-y:auto;padding:34px 46px 60px;display:none;z-index:20} | |
| #codex.show{display:block} | |
| #codex h2{color:var(--accent);margin:0 0 4px} | |
| #codex .cx{color:var(--dim);max-width:920px;margin-bottom:18px;line-height:1.6} | |
| #codex .cat{margin:20px 0 8px;color:var(--accent2);font-size:14px;letter-spacing:.5px; | |
| border-bottom:1px solid var(--line);padding-bottom:5px} | |
| #codex table{border-collapse:collapse;width:100%;max-width:1040px;font-size:12.5px} | |
| #codex td{border-bottom:1px solid #1a212c;padding:6px 10px;vertical-align:top} | |
| #codex td:first-child{color:var(--txt);font-weight:600;width:200px} | |
| #codex td:nth-child(2){color:var(--dim);width:210px;font-style:italic} | |
| #codex td:last-child{color:#aebbcd} | |
| #codexClose{position:fixed;top:18px;right:30px;z-index:21} | |
| .pipe{font-size:10.5px;color:var(--dim);line-height:1.7;margin-top:4px} | |
| .pipe .on{color:var(--accent)} | |
| ::-webkit-scrollbar{width:10px;height:10px} | |
| ::-webkit-scrollbar-thumb{background:#222a36;border-radius:5px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"> | |
| <div id="panel"> | |
| <h1>Planet<span>Lab</span></h1> | |
| <div class="sub">A complete world forge. Every slider is a real process that shapes a body's | |
| appearance from orbit — figure, illumination/phase, surface geology, ice, atmosphere, rings & | |
| moons. Open the <b>Codex</b> (top-right) for the full taxonomy.</div> | |
| <div class="sec open" data-sec> | |
| <div class="hd">World preset <span class="tg">▾</span></div> | |
| <div class="bd"> | |
| <select id="preset"></select> | |
| <div class="btnrow"> | |
| <button id="surprise" class="alt">🎲 Surprise me</button> | |
| <button id="reseed">↻ New seed</button> | |
| </div> | |
| <div class="row"><label>Seed</label><input type="number" id="seed" style="width:90px" value="1337"></div> | |
| <div class="pipe" id="pipe"></div> | |
| </div> | |
| </div> | |
| <div class="sec" data-sec><div class="hd">Figure & illumination <span class="tg">▾</span></div><div class="bd" id="grpFig"></div></div> | |
| <div class="sec" data-sec><div class="hd">Endogenic — internal forces <span class="tg">▾</span></div><div class="bd" id="grpEndo"></div></div> | |
| <div class="sec" data-sec><div class="hd">Exogenic — surface reshaping <span class="tg">▾</span></div><div class="bd" id="grpExo"></div></div> | |
| <div class="sec" data-sec><div class="hd">Cryo / ice / fluids <span class="tg">▾</span></div><div class="bd" id="grpCryo"></div></div> | |
| <div class="sec" data-sec><div class="hd">Albedo provinces (relief-free) <span class="tg">▾</span></div><div class="bd" id="grpAlb"></div></div> | |
| <div class="sec" data-sec><div class="hd">Atmosphere & clouds <span class="tg">▾</span></div><div class="bd" id="grpAtmo"></div></div> | |
| <div class="sec" data-sec><div class="hd">Rings & moons <span class="tg">▾</span></div><div class="bd" id="grpRing"></div></div> | |
| <div class="sec open" data-sec><div class="hd">Coloration & render <span class="tg">▾</span></div><div class="bd" id="grpColor"></div></div> | |
| <div class="btnrow"> | |
| <button id="build" class="primary" style="flex:2">⚙ Rebuild planet</button> | |
| <button id="save" class="alt">⬇ PNG</button> | |
| </div> | |
| </div> | |
| <div id="stage"> | |
| <canvas id="view" width="720" height="720"></canvas> | |
| <div id="hud"></div> | |
| <button id="codexBtn" class="alt">📖 Process Codex</button> | |
| <div id="spin"> | |
| <button id="spinToggle" class="alt">⏸ spin</button> | |
| <span>drag rotate · wheel zoom</span> | |
| </div> | |
| <div id="codex"> | |
| <button id="codexClose" class="primary">✕ close</button> | |
| <h2>The Planetary Process Codex — complete dimension set</h2> | |
| <div class="cx">A body's appearance from orbit is fully determined by a finite set of axes: | |
| its <b>figure</b>, the <b>illumination geometry</b> (phase/terminator), the <b>geological & | |
| atmospheric processes</b> that ran on it, its <b>coloration chemistry</b>, and its | |
| <b>companions</b> (rings, moons). PlanetLab now implements all of them. Below, ● = an adjustable | |
| layer in this build.</div> | |
| <div id="codexBody"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| "use strict"; | |
| /* ============================================================ * | |
| * PlanetLab v2 — complete procedural planetary appearance. | |
| * Pipeline: figure → noise base → endogenic → exogenic → cryo | |
| * → albedo provinces → color → render(phase, clouds, | |
| * rings, moons, atmosphere, emissive). | |
| * ============================================================ */ | |
| /* ---------- RNG + 3D simplex ---------- */ | |
| function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a); | |
| t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;};} | |
| const GRAD3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1], | |
| [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]]; | |
| let PERM=new Uint8Array(512),GP=new Uint8Array(512); | |
| function seedNoise(seed){ | |
| const r=mulberry32(seed>>>0);const p=new Uint8Array(256); | |
| for(let i=0;i<256;i++)p[i]=i; | |
| for(let i=255;i>0;i--){const j=(r()*(i+1))|0;const t=p[i];p[i]=p[j];p[j]=t;} | |
| for(let i=0;i<512;i++){PERM[i]=p[i&255];GP[i]=PERM[i]%12;} | |
| } | |
| const F3=1/3,G3=1/6; | |
| function snoise(x,y,z){ | |
| let n0,n1,n2,n3; | |
| const s=(x+y+z)*F3; | |
| const i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s); | |
| const t=(i+j+k)*G3; | |
| const x0=x-(i-t),y0=y-(j-t),z0=z-(k-t); | |
| let i1,j1,k1,i2,j2,k2; | |
| if(x0>=y0){ | |
| if(y0>=z0){i1=1;j1=0;k1=0;i2=1;j2=1;k2=0;} | |
| else if(x0>=z0){i1=1;j1=0;k1=0;i2=1;j2=0;k2=1;} | |
| else{i1=0;j1=0;k1=1;i2=1;j2=0;k2=1;} | |
| }else{ | |
| if(y0<z0){i1=0;j1=0;k1=1;i2=0;j2=1;k2=1;} | |
| else if(x0<z0){i1=0;j1=1;k1=0;i2=0;j2=1;k2=1;} | |
| else{i1=0;j1=1;k1=0;i2=1;j2=1;k2=0;} | |
| } | |
| const x1=x0-i1+G3,y1=y0-j1+G3,z1=z0-k1+G3; | |
| const x2=x0-i2+2*G3,y2=y0-j2+2*G3,z2=z0-k2+2*G3; | |
| const x3=x0-1+3*G3,y3=y0-1+3*G3,z3=z0-1+3*G3; | |
| const ii=i&255,jj=j&255,kk=k&255; | |
| let t0=0.6-x0*x0-y0*y0-z0*z0; | |
| if(t0<0)n0=0;else{const g=GRAD3[GP[ii+PERM[jj+PERM[kk]]]];t0*=t0;n0=t0*t0*(g[0]*x0+g[1]*y0+g[2]*z0);} | |
| let t1=0.6-x1*x1-y1*y1-z1*z1; | |
| if(t1<0)n1=0;else{const g=GRAD3[GP[ii+i1+PERM[jj+j1+PERM[kk+k1]]]];t1*=t1;n1=t1*t1*(g[0]*x1+g[1]*y1+g[2]*z1);} | |
| let t2=0.6-x2*x2-y2*y2-z2*z2; | |
| if(t2<0)n2=0;else{const g=GRAD3[GP[ii+i2+PERM[jj+j2+PERM[kk+k2]]]];t2*=t2;n2=t2*t2*(g[0]*x2+g[1]*y2+g[2]*z2);} | |
| let t3=0.6-x3*x3-y3*y3-z3*z3; | |
| if(t3<0)n3=0;else{const g=GRAD3[GP[ii+1+PERM[jj+1+PERM[kk+1]]]];t3*=t3;n3=t3*t3*(g[0]*x3+g[1]*y3+g[2]*z3);} | |
| return 32*(n0+n1+n2+n3); | |
| } | |
| function fbm(x,y,z,oct,lac,gain){let a=0.5,f=1,s=0,n=0;for(let o=0;o<oct;o++){s+=a*snoise(x*f,y*f,z*f);n+=a;a*=gain;f*=lac;}return s/n;} | |
| function ridged(x,y,z,oct,lac,gain){let a=0.5,f=1,s=0,n=0;for(let o=0;o<oct;o++){let v=1-Math.abs(snoise(x*f,y*f,z*f));v*=v;s+=a*v;n+=a;a*=gain;f*=lac;}return s/n;} | |
| /* ---------- math helpers ---------- */ | |
| function lerp(a,b,t){return a+(b-a)*t;} | |
| function clamp(v,a,b){return v<a?a:v>b?b:v;} | |
| function smooth(e0,e1,x){const t=clamp((x-e0)/(e1-e0||1),0,1);return t*t*(3-2*t);} | |
| function rampColor(stops,t){ | |
| t=clamp(t,0,1); | |
| for(let i=0;i<stops.length-1;i++){const a=stops[i],b=stops[i+1]; | |
| if(t>=a[0]&&t<=b[0]){const k=(t-a[0])/(b[0]-a[0]||1); | |
| return[lerp(a[1],b[1],k),lerp(a[2],b[2],k),lerp(a[3],b[3],k)];}} | |
| const l=stops[stops.length-1];return[l[1],l[2],l[3]]; | |
| } | |
| function nrm(v){const l=Math.hypot(v[0],v[1],v[2])||1;return[v[0]/l,v[1]/l,v[2]/l];} | |
| function mm3(A,B){const C=[[0,0,0],[0,0,0],[0,0,0]];for(let i=0;i<3;i++)for(let j=0;j<3;j++){let s=0;for(let k=0;k<3;k++)s+=A[i][k]*B[k][j];C[i][j]=s;}return C;} | |
| /* ============== PRESETS ============== */ | |
| const PRESETS={ | |
| "🔴 Mars — rusted volcanic world":{mode:"rocky",palette:"mars",oblate:.005, | |
| tectonics:.55,mountains:.6,scarps:.25,wrinkle:.2,volcano:.7,floods:.35, | |
| craters:.45,craterSize:.6,relaxation:.1,multiring:.3,secondaries:.4, | |
| erosion:.4,dunes:.4,stratify:.4,weather:.15,sea:0,iceCap:.18,seasonalFrost:.4, | |
| oxidation:.85,relief:1.1,albedoProv:.4,slopeStreaks:.5,dustStorm:.0, | |
| atmThickness:.12,rayleigh:.12,atmoGlow:.5,rivers:.35,masswaste:.4,windStreaks:.6, | |
| yardangs:.5,rifts:.35,castShadow:.4,pldSpiral:.5,plumeSwell:.6,degradation:.3, | |
| devilTracks:.5,latBands:.3,inverted:.25,ambient:.04}, | |
| "🌑 Moon — airless & cratered":{mode:"airless",palette:"moon", | |
| tectonics:.15,mountains:.25,wrinkle:.4,craters:.95,craterSize:.7,relaxation:0, | |
| multiring:.6,secondaries:.6,volcano:.1,floods:.6,weather:.7,relief:1.3, | |
| albedoProv:.7,dichotomy:.3,oppSurge:.6,swirls:.6,castShadow:.55,lommel:.7,degradation:.45,ambient:.02,atmoGlow:0}, | |
| "☿ Mercury — scorched, shrunk":{mode:"airless",palette:"mercury", | |
| tectonics:.4,mountains:.3,scarps:.85,craters:.9,craterSize:.6,multiring:.5, | |
| secondaries:.5,volcano:.15,floods:.4,weather:.55,relief:1.2,oxidation:.05, | |
| albedoProv:.5,ambient:.02,atmoGlow:0}, | |
| "🌍 Earth-like — ocean world":{mode:"rocky",palette:"earth",oblate:.01, | |
| tectonics:.7,mountains:.7,scarps:.15,volcano:.3,floods:.1,craters:.04, | |
| erosion:.7,dunes:.2,sea:.52,iceCap:.4,seasonalFrost:.2,oxidation:.1,weather:.05, | |
| relief:.9,clouds:.6,cloudOpacity:.6,cloudShadows:.4,cityLights:.5,albedoProv:.3, | |
| atmThickness:.25,rayleigh:.7,atmoGlow:1.1,rivers:.6,masswaste:.3,cyclones:.6, | |
| multiDeck:.4,lightning:.3,castShadow:.25,coastlines:.6,glacial:.3,ambient:.05}, | |
| "🟡 Venus — cloud-shrouded":{mode:"rocky",palette:"venus", | |
| tectonics:.5,mountains:.5,volcano:.85,floods:.55,craters:.1,craterSize:.5, | |
| erosion:.15,oxidation:.3,relief:.95,clouds:1,cloudOpacity:1,haze:1, | |
| atmThickness:.7,limbDarkening:.25,atmoGlow:1.6,coronae:.7,tesserae:.7,cyclones:.2, | |
| plumeSwell:.5,volcanicPlains:.6,ambient:.06}, | |
| "🪐 Saturn — ringed gas giant":{mode:"gas",palette:"saturn",oblate:.1,axialTilt:27, | |
| bands:11,bandWarp:.35,bandIrregular:.6,altColor:.6,storms:.4,hexagon:1, | |
| polarVortex:.4,bandContrast:.7,rings:1,ringInner:1.25,ringOuter:2.35, | |
| ringOpacity:.78,ringSpokes:.5,ringForward:.5,limbDarkening:.45,atmoGlow:.9,relief:.22,ambient:.04}, | |
| "🪐 Jupiter — banded gas giant":{mode:"gas",palette:"jupiter",oblate:.065, | |
| bands:13,bandWarp:.5,bandIrregular:.7,altColor:.7,storms:.7,redspot:1, | |
| polarVortex:.7,bandContrast:.85,limbDarkening:.5,atmoGlow:1.3,relief:.25,moons:2,ambient:.04}, | |
| "🔵 Neptune — windy ice giant":{mode:"gas",palette:"neptune",oblate:.017, | |
| bands:5,bandWarp:.3,bandIrregular:.3,altColor:.3,storms:.4,darkspot:1, | |
| bandContrast:.4,limbDarkening:.5,atmoGlow:1.5,relief:.18,ambient:.04}, | |
| "🩵 Uranus — sideways ice giant":{mode:"gas",palette:"uranus",oblate:.022,axialTilt:98, | |
| bands:3,bandWarp:.08,bandIrregular:.1,altColor:.1,storms:.05,bandContrast:.08, | |
| rings:1,ringInner:1.5,ringOuter:1.95,ringOpacity:.32,limbDarkening:.55,atmoGlow:1.5,relief:.1,ambient:.04}, | |
| "🧊 Europa — fractured ice shell":{mode:"icy",palette:"europa", | |
| tectonics:.1,mountains:.1,craters:.08,craterSize:.4,relaxation:.5,floods:.25, | |
| erosion:.2,linea:.6,doubleRidge:.8,chaos:.5,iceCap:.1,relief:.5, | |
| albedoProv:.3,polarTholin:0,oxidation:0,weather:.05,ambient:.05,atmoGlow:.2}, | |
| "❄ Enceladus — tiger-stripe geysers":{mode:"icy",palette:"enceladus", | |
| tectonics:.15,craters:.2,craterSize:.5,relaxation:.6,floods:.2,erosion:.15, | |
| linea:.3,tiger:.8,iceCap:.2,relief:.6,weather:.02,ambient:.05,atmoGlow:.15}, | |
| "🟤 Ganymede — grooved terrain":{mode:"icy",palette:"ganymede", | |
| tectonics:.3,craters:.6,craterSize:.55,relaxation:.55,multiring:.6,sulci:.85, | |
| floods:.2,erosion:.15,iceCap:.1,weather:.3,relief:.6,albedoProv:.5,ambient:.03}, | |
| "⚫ Callisto — saturated palimpsests":{mode:"icy",palette:"callisto", | |
| tectonics:.1,craters:.95,craterSize:.6,relaxation:.7,multiring:.85,floods:.1, | |
| weather:.6,relief:.7,albedoProv:.4,ambient:.03}, | |
| "🌗 Iapetus — two-tone walnut":{mode:"icy",palette:"iapetus",oblate:.03, | |
| tectonics:.1,craters:.8,craterSize:.6,relaxation:.4,multiring:.4,equatorRidge:1, | |
| hemiAlbedo:1,floods:.05,weather:.4,relief:.7,ambient:.03}, | |
| "🌋 Io — sulfur volcano hell":{mode:"rocky",palette:"io", | |
| tectonics:.2,mountains:.3,volcano:1,floods:.5,craters:0,erosion:.1,dunes:.1, | |
| sulfur:1,relief:1,albedoProv:.6,ambient:.05}, | |
| "🟠 Titan — methane & dunes":{mode:"rocky",palette:"titan", | |
| tectonics:.25,mountains:.2,volcano:.1,floods:.1,craters:.1,erosion:.6,dunes:.85, | |
| sea:.12,hydrocarbon:1,iceCap:.1,tholin:1,relief:.6,clouds:.35,cloudOpacity:.5, | |
| haze:.7,hazeLayers:.7,atmThickness:.6,limbDarkening:.2,atmoGlow:1.1,rivers:.5,polarHood:.6,ambient:.05}, | |
| "🌫 Pluto — frozen tholin dwarf":{mode:"icy",palette:"pluto", | |
| tectonics:.2,craters:.4,craterSize:.55,relaxation:.5,nitrogenGlacier:.8, | |
| sublimation:.6,floods:.2,erosion:.15,iceCap:.3,linea:.1,polarTholin:.7, | |
| weather:.3,relief:.7,albedoProv:.5,hazeLayers:.8,atmThickness:.2,glacialFlow:.6,ambient:.04,atmoGlow:.4}, | |
| "🥔 Vesta — irregular protoplanet":{mode:"airless",palette:"vesta", | |
| irregular:.55,oblate:.12,tectonics:.2,mountains:.4,craters:.9,craterSize:.7, | |
| multiring:.5,equatorRidge:.4,weather:.4,relief:1.4,albedoProv:.5,ambient:.03}, | |
| "☄ Asteroid — rubble potato":{mode:"airless",palette:"vesta", | |
| irregular:1,lobes:1,tectonics:.1,mountains:.2,craters:.85,craterSize:.8, | |
| weather:.5,relief:1.6,albedoProv:.4,ambient:.02}, | |
| "🏜 Desert / dune world":{mode:"rocky",palette:"desert", | |
| tectonics:.4,mountains:.45,volcano:.15,floods:.1,craters:.1,erosion:.3,dunes:1, | |
| stratify:.3,sea:0,iceCap:.05,oxidation:.5,relief:.85,albedoProv:.4,atmoGlow:.6,ambient:.05}, | |
| /* ===== Beyond the solar system — same dimensions, different settings ===== */ | |
| "🌶 Hot Jupiter — tidally-locked giant":{mode:"gas",palette:"hotjup",oblate:.08, | |
| bands:8,bandWarp:.5,bandIrregular:.6,altColor:.5,storms:.6,bandContrast:.7, | |
| tidalLock:1,lavaWorld:.45,atmoGlow:1.4,relief:.2,ambient:.02}, | |
| "🌎 Super-Earth — massive rocky":{mode:"rocky",palette:"earth",oblate:.008, | |
| tectonics:.9,mountains:.85,scarps:.2,volcano:.5,floods:.15,craters:.03, | |
| erosion:.8,dunes:.2,sea:.5,iceCap:.3,seasonalFrost:.2,clouds:.5,cloudOpacity:.55, | |
| oxidation:.1,relief:1.0,albedoProv:.3,atmoGlow:1.1,cityLights:.0,ambient:.05}, | |
| "🌊 Ocean world — global sea":{mode:"rocky",palette:"ocean", | |
| tectonics:.4,mountains:.5,volcano:.2,floods:.1,craters:.03,erosion:.5, | |
| sea:.92,iceCap:.25,clouds:.55,cloudOpacity:.6,relief:.7,atmoGlow:1.2,ambient:.05}, | |
| "♨ Hycean world — hot ocean + H₂ haze":{mode:"rocky",palette:"ocean", | |
| tectonics:.3,mountains:.4,volcano:.15,craters:.02,erosion:.4,sea:.97,iceCap:.04, | |
| clouds:.8,cloudOpacity:.75,tholin:.1,relief:.5,haze:.5,hazeLayers:.5, | |
| atmThickness:.5,rayleigh:.25,atmoGlow:1.8,ambient:.06}, | |
| "🔥 Lava world — magma ocean":{mode:"rocky",palette:"lavaworld", | |
| tectonics:.4,mountains:.5,volcano:1,floods:.6,craters:.05,erosion:.05, | |
| lavaWorld:1,relief:1.0,albedoProv:.3,atmoGlow:.8,ambient:.03}, | |
| "👁 Eyeball world — locked ice + substellar sea":{mode:"rocky",palette:"ocean", | |
| tectonics:.3,mountains:.4,volcano:.15,erosion:.3,sea:.3,tidalLock:1,frozen:1, | |
| clouds:.3,cloudOpacity:.4,relief:.7,atmoGlow:1.0,ambient:.04}, | |
| "💎 Carbon / diamond world":{mode:"rocky",palette:"carbon", | |
| tectonics:.4,mountains:.5,volcano:.3,floods:.2,craters:.5,craterSize:.6, | |
| erosion:.2,dunes:.2,weather:.4,relief:1.0,albedoProv:.4,atmoGlow:.3,ambient:.03}, | |
| "⚙ Super-Io — tidally-heated exomoon":{mode:"rocky",palette:"io", | |
| tectonics:.3,mountains:.4,volcano:1,floods:.6,craters:0,erosion:.1,sulfur:1, | |
| lavaWorld:.35,relief:1.1,albedoProv:.6,atmoGlow:.3,ambient:.04}, | |
| "🫧 Mini-Neptune — hazy envelope":{mode:"gas",palette:"subneptune",oblate:.03, | |
| bands:4,bandWarp:.15,bandIrregular:.2,altColor:.15,storms:.1,bandContrast:.12, | |
| atmoGlow:1.6,relief:.1,ambient:.04}, | |
| "❄ Snowball world — fully glaciated":{mode:"rocky",palette:"snowball", | |
| tectonics:.5,mountains:.6,volcano:.1,erosion:.4,sea:.5,frozen:1,iceCap:.5, | |
| clouds:.4,cloudOpacity:.5,relief:.8,atmoGlow:1.0,ambient:.05}, | |
| "🪨 Chthonian — stripped hot core":{mode:"rocky",palette:"chthonian", | |
| tectonics:.5,mountains:.5,volcano:.7,floods:.5,craters:.2,erosion:.1, | |
| lavaWorld:.5,tidalLock:.6,oxidation:.2,relief:1.0,atmoGlow:.4,ambient:.03}, | |
| "🌑 Rogue planet — starless drifter":{mode:"rocky",palette:"callisto", | |
| tectonics:.3,mountains:.4,craters:.5,craterSize:.6,erosion:.1,frozen:1,iceCap:.4, | |
| weather:.4,relief:.8,sunEl:-5,atmoGlow:0,ambient:.13}, | |
| "☄️ Comet — active icy nucleus":{mode:"airless",palette:"callisto", | |
| irregular:.9,lobes:.5,tectonics:.1,mountains:.2,craters:.4,craterSize:.7, | |
| sublimation:.6,weather:.3,relief:1.5,tail:1,albedoProv:.4,sunEl:10,ambient:.04}, | |
| "🌬 Cometary planet — evaporating hot Neptune":{mode:"gas",palette:"subneptune",oblate:.05, | |
| bands:5,bandWarp:.25,bandIrregular:.3,altColor:.2,storms:.2,bandContrast:.25, | |
| tidalLock:.7,tail:1,atmoGlow:1.5,relief:.12,ambient:.03} | |
| }; | |
| const PALETTES={ | |
| mars:[[0,60,28,18],[.35,120,55,32],[.55,165,82,46],[.75,200,120,70],[.92,225,170,130],[1,240,225,210]], | |
| moon:[[0,42,42,48],[.4,80,80,88],[.6,120,120,128],[.8,165,165,172],[1,210,210,214]], | |
| mercury:[[0,55,48,44],[.4,95,84,76],[.65,140,122,108],[.85,180,160,142],[1,215,200,185]], | |
| earth:[[0,40,90,70],[.4,70,130,75],[.6,110,150,80],[.72,150,140,100],[.85,120,95,70],[1,235,238,242]], | |
| venus:[[0,90,60,28],[.4,150,110,50],[.65,190,150,75],[.85,215,185,120],[1,235,215,170]], | |
| europa:[[0,150,140,130],[.45,205,195,185],[.7,228,222,216],[1,245,243,240]], | |
| enceladus:[[0,200,205,212],[.5,230,234,240],[1,250,252,255]], | |
| ganymede:[[0,90,80,72],[.45,140,128,116],[.7,180,170,158],[1,212,205,196]], | |
| callisto:[[0,55,48,44],[.4,95,84,74],[.7,140,124,108],[1,185,168,150]], | |
| iapetus:[[0,60,52,44],[.5,150,138,122],[1,225,218,205]], | |
| io:[[0,90,70,20],[.3,180,150,40],[.5,225,200,70],[.7,235,170,55],[.85,200,90,40],[1,245,230,150]], | |
| titan:[[0,90,55,25],[.35,140,95,45],[.6,175,130,70],[.8,200,160,100],[1,220,195,150]], | |
| jupiter:[[0,150,110,80],[.5,210,185,150],[1,245,238,225]], | |
| saturn:[[0,180,150,110],[.5,220,200,160],[1,245,235,210]], | |
| neptune:[[0,30,55,120],[.5,45,90,170],[1,120,175,230]], | |
| uranus:[[0,150,200,205],[.5,175,215,218],[1,205,233,233]], | |
| desert:[[0,120,85,50],[.4,170,130,80],[.65,205,170,115],[.85,225,200,150],[1,240,225,195]], | |
| pluto:[[0,90,72,60],[.4,140,115,98],[.65,180,160,140],[.85,210,195,178],[1,238,232,222]], | |
| vesta:[[0,70,62,54],[.45,120,108,94],[.7,160,146,128],[1,200,186,166]], | |
| // ---- exoplanet / theoretical worlds ---- | |
| lavaworld:[[0,28,14,11],[.4,60,30,18],[.7,110,55,30],[1,160,95,55]], | |
| carbon:[[0,16,16,20],[.5,38,38,44],[.8,70,70,78],[1,110,110,118]], | |
| hotjup:[[0,110,55,48],[.5,200,120,88],[1,248,212,182]], | |
| subneptune:[[0,80,105,118],[.5,135,158,162],[1,195,210,210]], | |
| chthonian:[[0,38,22,16],[.5,90,55,38],[.8,150,100,70],[1,195,150,115]], | |
| snowball:[[0,195,212,228],[.5,222,233,244],[1,248,250,255]], | |
| ocean:[[0,8,34,72],[.45,18,66,116],[.62,34,96,150],[.8,90,150,190],[1,190,215,238]] | |
| }; | |
| /* ============== STATE ============== */ | |
| const D={ | |
| seed:1337,preset:Object.keys(PRESETS)[0],mode:"rocky",palette:"mars", | |
| // figure & illumination | |
| oblate:0,irregular:0,lobes:0,axialTilt:0,sunAz:300,sunEl:18,ambient:.04,relief:1.1, | |
| // circumplanetary extras | |
| aurora:0,tail:0, | |
| // endogenic | |
| tectonics:.55,mountains:.6,scarps:0,wrinkle:0,volcano:.7,floods:.35, | |
| // exogenic | |
| craters:.45,craterSize:.6,relaxation:0,multiring:0,secondaries:0, | |
| erosion:.4,dunes:.4,stratify:0,weather:.15, | |
| // cryo | |
| sea:0,iceCap:.18,seasonalFrost:0,linea:0,doubleRidge:0,tiger:0,sulci:0, | |
| chaos:0,nitrogenGlacier:0,sublimation:0,hydrocarbon:0,equatorRidge:0, | |
| // albedo provinces | |
| albedoProv:0,dichotomy:0,hemiAlbedo:0,polarTholin:0,slopeStreaks:0, | |
| // atmosphere & clouds | |
| clouds:0,cloudOpacity:.6,dustStorm:0,atmoGlow:0,cityLights:0,oppSurge:0, | |
| atmThickness:0,rayleigh:0,haze:0,hazeLayers:0,limbDarkening:0,cloudShadows:0, | |
| // gas | |
| bands:9,bandWarp:.45,bandIrregular:.5,altColor:.5,storms:.7,redspot:0, | |
| darkspot:0,polarVortex:0,hexagon:0,bandContrast:.6, | |
| // rings & moons | |
| rings:0,ringInner:1.3,ringOuter:2.2,ringTilt:.5,ringOpacity:.85,moons:0, | |
| // exoworld physics | |
| tidalLock:0,lavaWorld:0,frozen:0, | |
| // pass-2 processes: erosion family, regional structure, albedo, cryo detail, optics | |
| rivers:0,masswaste:0,rifts:0,coronae:0,tesserae:0,yardangs:0, | |
| windStreaks:0,swirls:0,provinces:0,faculae:0, | |
| cryoDome:0,cantaloupe:0,wispy:0,polygons:0,pldSpiral:0, | |
| castShadow:0,starTemp:.5,earthshine:0,lommel:0,cyclones:0,multiDeck:0,lightning:0,ringSpokes:0,polarHood:0, | |
| // pass-3: regional structure, glacial/coastal, freeze-state, optics | |
| plumeSwell:0,volcanicPlains:0,glacial:0,coastlines:0,inverted:0,degradation:0, | |
| glacialFlow:0,frozenSea:0,seaIce:0,spiders:0,latBands:0,devilTracks:0,ringForward:0, | |
| // coloration | |
| oxidation:0,tholin:0,sulfur:0 | |
| }; | |
| const S=Object.assign({},D); | |
| /* ============== MAPS ============== */ | |
| const W=1024,H=512; | |
| const Hf=new Float32Array(W*H); // elevation | |
| const Ab=new Float32Array(W*H); // albedo multiplier | |
| const Tg=new Uint8Array(W*H); // 0 rock,1 ocean,2 mare/lava,3 ice,4 fracture,5 sand,6 hcarbon-lake,7 n2-glacier | |
| const Rb=new Float32Array(W*H); // brightness add (rays) | |
| const Cl=new Float32Array(W*H); // cloud density (upper deck) | |
| const Cl2=new Float32Array(W*H); // cloud density (lower deck) | |
| const Shp=new Float32Array(W*H); // figure radial multiplier (irregular bodies) | |
| const En=new Float32Array(W*H); // emissive (city lights) | |
| const colR=new Uint8ClampedArray(W*H),colG=new Uint8ClampedArray(W*H),colB=new Uint8ClampedArray(W*H); | |
| const Nx=new Float32Array(W*H),Ny=new Float32Array(W*H),Nz=new Float32Array(W*H); | |
| let HEMI=[1,0,0]; // dichotomy / albedo hemisphere axis (model space) | |
| let WIND=[1,0,0]; // global prevailing wind direction (model space) | |
| function dirOf(x,y){ | |
| const lon=(x/W)*2*Math.PI-Math.PI, lat=Math.PI/2-(y/H)*Math.PI, cl=Math.cos(lat); | |
| return[cl*Math.sin(lon),Math.sin(lat),cl*Math.cos(lon)]; | |
| } | |
| function texelOf(d){ | |
| const lon=Math.atan2(d[0],d[2]), lat=Math.asin(clamp(d[1],-1,1)); | |
| let u=((lon+Math.PI)/(2*Math.PI))*W|0; if(u<0)u=0;if(u>=W)u=W-1; | |
| let v=((Math.PI/2-lat)/Math.PI)*H|0; if(v<0)v=0;if(v>=H)v=H-1; | |
| return v*W+u; | |
| } | |
| function randDir(rng){const z=rng()*2-1,a=rng()*2*Math.PI,r=Math.sqrt(1-z*z);return[r*Math.cos(a),z,r*Math.sin(a)];} | |
| /* ============== BUILD ============== */ | |
| function build(){ | |
| const t0=performance.now(); | |
| seedNoise(S.seed); | |
| const rng=mulberry32(S.seed^0x9e37); | |
| Hf.fill(0);Ab.fill(1);Tg.fill(0);Rb.fill(0);Cl.fill(0);Shp.fill(1);En.fill(0); | |
| HEMI=randDir(rng); | |
| if(S.mode==="gas") buildGas(rng); else buildSolid(rng); | |
| if(S.irregular>0) buildShape(rng); | |
| buildClouds(rng); | |
| computeNormals(S.mode==="gas"); | |
| computeColor(S.mode==="gas"); | |
| buildEmissive(); | |
| drawPipe(); | |
| hud(`built in ${(performance.now()-t0)|0} ms`); | |
| } | |
| function normField(){let mn=1e9,mx=-1e9;for(let i=0;i<Hf.length;i++){if(Hf[i]<mn)mn=Hf[i];if(Hf[i]>mx)mx=Hf[i];} | |
| const r=mx-mn||1;for(let i=0;i<Hf.length;i++)Hf[i]=(Hf[i]-mn)/r;} | |
| function buildSolid(rng){ | |
| const cont=S.tectonics,mtn=S.mountains; | |
| WIND=randDir(rng); | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){ | |
| const d=dirOf(x,y),i=y*W+x; | |
| let h=fbm(d[0]*1.4,d[1]*1.4,d[2]*1.4,6,2.0,.55); | |
| h+=0.4*fbm(d[0]*3.5,d[1]*3.5,d[2]*3.5,5,2.1,.5); | |
| let r=ridged(d[0]*2.2,d[1]*2.2,d[2]*2.2,5,2.0,.55); | |
| h=h*(0.4+0.6*cont)+(r-0.4)*1.3*mtn; | |
| // hemispheric dichotomy: degree-1 elevation offset along HEMI axis | |
| if(S.dichotomy>0){const dot=d[0]*HEMI[0]+d[1]*HEMI[1]+d[2]*HEMI[2];h+=dot*0.45*S.dichotomy;} | |
| Hf[i]=h; | |
| } | |
| normField(); | |
| if(S.craters>0) stampCraters(rng); | |
| if(S.plumeSwell>0)plumeSwell(rng); | |
| if(S.volcano>0) stampVolcanoes(rng); | |
| if(S.floods>0) floodBasins(); | |
| if(S.volcanicPlains>0)volcanicPlains(); | |
| if(S.wrinkle>0) wrinkleRidges(); | |
| if(S.scarps>0) lobateScarps(rng); | |
| if(S.rifts>0) riftGrabens(rng); | |
| if(S.coronae>0) coronae(rng); | |
| if(S.tesserae>0)tesserae(); | |
| if(S.erosion>0) thermalErode(Math.round(S.erosion*14)); | |
| if(S.degradation>0)degradeCraters(S.degradation); | |
| if(S.rivers>0) flowErode(S.rivers); | |
| if(S.masswaste>0)massWaste(S.masswaste); | |
| if(S.glacial>0) glacialErode(S.glacial); | |
| if(S.stratify>0)stratify(); | |
| if(S.dunes>0) applyDunes(); | |
| if(S.yardangs>0)yardangs(); | |
| if(S.coastlines>0)coastlines(); | |
| if(S.inverted>0)invertedRelief(); | |
| // cryo / ice refinements | |
| if(S.linea>0) stampLineae(rng); | |
| if(S.tiger>0) stampTigerStripes(rng); | |
| if(S.sulci>0) stampSulci(rng); | |
| if(S.chaos>0) applyChaos(rng); | |
| if(S.cryoDome>0) cryoDomes(rng); | |
| if(S.cantaloupe>0) cantaloupe(); | |
| if(S.nitrogenGlacier>0) nitrogenGlacier(); | |
| if(S.glacialFlow>0) glacialFlow(); | |
| if(S.sublimation>0) sublimation(); | |
| if(S.equatorRidge>0) equatorRidge(); | |
| if(S.polygons>0) polygonCracks(); | |
| if(S.wispy>0) wispyTerrain(rng); | |
| tagOceanIce(); | |
| if(S.frozenSea>0) frozenSea(); | |
| if(S.seaIce>0) seaIce(); | |
| if(S.spiders>0) spiders(rng); | |
| if(S.pldSpiral>0) pldSpiral(); | |
| // relief-free albedo provinces (overlay after tagging) | |
| if(S.windStreaks>0) windStreaks(); | |
| if(S.swirls>0) magSwirls(); | |
| if(S.provinces>0) sharpProvinces(rng); | |
| if(S.faculae>0) faculae(rng); | |
| } | |
| /* impact craters w/ relaxation, multiring, secondaries, dichotomy density */ | |
| function stampCraters(rng){ | |
| let n=Math.round(S.craters*420); | |
| for(let c=0;c<n;c++){ | |
| let cd=randDir(rng); | |
| // dichotomy: bias more craters to the "old" hemisphere | |
| if(S.dichotomy>0){const dot=cd[0]*HEMI[0]+cd[1]*HEMI[1]+cd[2]*HEMI[2]; | |
| if(dot>0 && rng()<S.dichotomy*0.6){cd=randDir(rng);}} | |
| const big=rng()<0.12; | |
| let rad=(0.006+rng()*0.05*S.craterSize)*(big?2.4:1); | |
| const depth=rad*(0.7+rng()*0.5); | |
| const fresh=rng()<0.18; | |
| const peak=big&&rng()<0.6; | |
| const relax=S.relaxation*(0.4+rng()*0.6); // 0=sharp,1=flat palimpsest | |
| stampCrater(cd,rad,depth,fresh,peak,relax,c); | |
| // multi-ring basins for the largest impacts | |
| if(big&&S.multiring>0&&rng()<S.multiring){ | |
| const nr=2+(S.multiring*3|0); | |
| for(let k=1;k<=nr;k++){const rr=rad*(1+k*0.6); | |
| ringGraben(cd,rr,depth*0.25*(1-k/(nr+1)));} | |
| } | |
| // secondary crater chains | |
| if(fresh&&S.secondaries>0){ | |
| const ns=(S.secondaries*14)|0; | |
| for(let s=0;s<ns;s++){ | |
| const t=rad*(1.4+rng()*3);const br=rng()*2*Math.PI; | |
| const sd=rotAround(cd,br,t); | |
| stampCrater(sd,rad*(0.15+rng()*0.2),depth*0.2,false,false,0.2,c*7+s); | |
| } | |
| } | |
| } | |
| } | |
| function stampCrater(cd,rad,depth,fresh,peak,relax,seed){ | |
| const cx=Math.atan2(cd[0],cd[2]),cy=Math.asin(cd[1]); | |
| const px=((cx+Math.PI)/(2*Math.PI))*W,py=((Math.PI/2-cy)/Math.PI)*H; | |
| const span=Math.ceil(rad*1.9/Math.PI*H)+2; | |
| for(let dy=-span;dy<=span;dy++){let yy=(py+dy)|0;if(yy<0||yy>=H)continue; | |
| const latw=Math.max(0.15,Math.cos(Math.PI/2-(yy/H)*Math.PI)); | |
| const xspan=Math.ceil(span/latw)+1; | |
| for(let dx=-xspan;dx<=xspan;dx++){let xx=((px+dx)%W+W)%W|0; | |
| const d=dirOf(xx,yy); | |
| const dot=clamp(d[0]*cd[0]+d[1]*cd[1]+d[2]*cd[2],-1,1); | |
| const t=Math.acos(dot)/rad,i=yy*W+xx; | |
| if(t<1.3){ | |
| const rim=depth*0.55*Math.exp(-((t-1)/0.18)*((t-1)/0.18)); | |
| const bowl=t<1?-depth*(1-t*t):0; | |
| let dh=rim+bowl; | |
| if(peak&&t<0.22)dh+=depth*0.8*(1-(t/0.22)*(t/0.22)); | |
| Hf[i]+=dh*(1-relax*0.85); // viscous relaxation flattens relief | |
| Ab[i]*=fresh?lerp(1,1.4,clamp(1-t,0,1)):lerp(1,1.05,clamp(1-t,0,1)); | |
| if(relax>0.4&&t<1)Ab[i]*=1.04; // palimpsest = bright circular patch | |
| } | |
| if(fresh&&t>=1&&t<6){ | |
| const bearing=Math.atan2(d[1]-cd[1],d[0]-cd[0]); | |
| const ray=Math.pow(Math.abs(Math.sin(bearing*7+seed)),8); | |
| if(ray>0.4)Rb[i]+=ray*0.5*Math.exp(-t/3); | |
| } | |
| } | |
| } | |
| } | |
| function ringGraben(cd,rad,amp){ | |
| const px=((Math.atan2(cd[0],cd[2])+Math.PI)/(2*Math.PI))*W,py=((Math.PI/2-Math.asin(cd[1]))/Math.PI)*H; | |
| const span=Math.ceil(rad*1.3/Math.PI*H)+2; | |
| for(let dy=-span;dy<=span;dy++){let yy=(py+dy)|0;if(yy<0||yy>=H)continue; | |
| const latw=Math.max(0.15,Math.cos(Math.PI/2-(yy/H)*Math.PI)),xspan=Math.ceil(span/latw)+1; | |
| for(let dx=-xspan;dx<=xspan;dx++){let xx=((px+dx)%W+W)%W|0; | |
| const d=dirOf(xx,yy);const ang=Math.acos(clamp(d[0]*cd[0]+d[1]*cd[1]+d[2]*cd[2],-1,1)); | |
| const dd=Math.abs(ang-rad);if(dd<0.02){const i=yy*W+xx;Hf[i]-=amp*(1-dd/0.02);}}} | |
| } | |
| function rotAround(axis,ang,dist){ // return dir at angular dist from axis at bearing ang | |
| let t=[axis[1],axis[2],axis[0]];const e=nrm([axis[1]*t[2]-axis[2]*t[1],axis[2]*t[0]-axis[0]*t[2],axis[0]*t[1]-axis[1]*t[0]]); | |
| const f=nrm([axis[1]*e[2]-axis[2]*e[1],axis[2]*e[0]-axis[0]*e[2],axis[0]*e[1]-axis[1]*e[0]]); | |
| const sd=Math.sin(dist),cd2=Math.cos(dist),ca=Math.cos(ang),sa=Math.sin(ang); | |
| return nrm([axis[0]*cd2+sd*(e[0]*ca+f[0]*sa),axis[1]*cd2+sd*(e[1]*ca+f[1]*sa),axis[2]*cd2+sd*(e[2]*ca+f[2]*sa)]); | |
| } | |
| function stampVolcanoes(rng){ | |
| const n=Math.round(S.volcano*(S.sulfur?60:22)); | |
| for(let c=0;c<n;c++){ | |
| const cd=randDir(rng),rad=0.04+rng()*0.10,hgt=(0.12+rng()*0.18)*S.relief; | |
| const px=((Math.atan2(cd[0],cd[2])+Math.PI)/(2*Math.PI))*W,py=((Math.PI/2-Math.asin(cd[1]))/Math.PI)*H; | |
| const span=Math.ceil(rad*1.6/Math.PI*H)+2; | |
| for(let dy=-span;dy<=span;dy++){let yy=(py+dy)|0;if(yy<0||yy>=H)continue; | |
| const latw=Math.max(0.15,Math.cos(Math.PI/2-(yy/H)*Math.PI)),xspan=Math.ceil(span/latw)+1; | |
| for(let dx=-xspan;dx<=xspan;dx++){let xx=((px+dx)%W+W)%W|0; | |
| const d=dirOf(xx,yy);const t=Math.acos(clamp(d[0]*cd[0]+d[1]*cd[1]+d[2]*cd[2],-1,1))/rad,i=yy*W+xx; | |
| if(t<1.4){Hf[i]+=hgt*Math.exp(-t*t*2.2); | |
| if(t<0.14){Hf[i]-=hgt*0.32;Tg[i]=2;Ab[i]*=0.55;} // collapse caldera | |
| else if(t<0.9&&S.sulfur)Tg[i]=2; | |
| else if(t<1.1&&!S.sulfur)Ab[i]*=lerp(1,0.85,1-t); } // pyroclastic mantle | |
| }} | |
| } | |
| normField(); | |
| } | |
| function floodBasins(){ | |
| const level=lerp(0.18,0.5,S.floods); | |
| for(let i=0;i<Hf.length;i++)if(Hf[i]<level&&Tg[i]!==3){Hf[i]=level-(level-Hf[i])*0.12;Tg[i]=2;Ab[i]*=0.62;} | |
| } | |
| function wrinkleRidges(){ // branching compressional ridges on mare plains | |
| const amp=S.wrinkle*0.012; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]!==2)continue; | |
| const d=dirOf(x,y);const r=ridged(d[0]*9+5,d[1]*9,d[2]*9+5,3,2.2,.5); | |
| if(r>0.55)Hf[i]+=(r-0.55)*amp*6;} | |
| } | |
| function lobateScarps(rng){ // one-sided thrust scarps (planetary contraction) | |
| const n=Math.round(S.scarps*18); | |
| for(let c=0;c<n;c++){const pn=randDir(rng),off=(rng()-0.5)*1.2,amp=0.02*S.scarps; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y); | |
| const s=Math.asin(clamp(d[0]*pn[0]+d[1]*pn[1]+d[2]*pn[2],-1,1))-off*0.3; | |
| const w=0.05;if(s>-w&&s<w){const i=y*W+x;Hf[i]+=amp*smooth(-w,w,s);}}} | |
| } | |
| function thermalErode(iter){ | |
| const talus=0.004,k=0.5; | |
| for(let it=0;it<iter;it++)for(let y=1;y<H-1;y++){const yo=y*W; | |
| for(let x=0;x<W;x++){const i=yo+x,l=yo+((x-1+W)%W),r=yo+((x+1)%W),u=i-W,dn=i+W; | |
| let dmax=0,tgt=-1;for(const j of[l,r,u,dn]){const df=Hf[i]-Hf[j];if(df>dmax){dmax=df;tgt=j;}} | |
| if(dmax>talus){const mv=(dmax-talus)*k*0.5;Hf[i]-=mv;Hf[tgt]+=mv;}}} | |
| } | |
| function stratify(){ // sedimentary / polar layered banding (albedo + micro-terrace) | |
| const a=S.stratify; | |
| for(let i=0;i<Hf.length;i++){const band=Math.sin(Hf[i]*60);Ab[i]*=1+band*0.10*a;Hf[i]+=band*0.0025*a;} | |
| } | |
| function applyDunes(){ | |
| const amp=S.dunes*0.05; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]===2||Tg[i]===3)continue; | |
| const d=dirOf(x,y);const warp=fbm(d[0]*1.5,d[1]*1.5,d[2]*1.5,3,2,.5); | |
| const ripple=Math.sin((d[0]*40+d[2]*30)+warp*6);const flat=clamp(1-Math.abs(Hf[i]-0.4)*3,0,1); | |
| if(ripple>0){Hf[i]+=ripple*amp*flat;if(!Tg[i])Tg[i]=5;}} | |
| } | |
| /* tidal lineae + Europa double-ridges / cycloids */ | |
| function stampLineae(rng){ | |
| const n=Math.round(S.linea*40),dr=S.doubleRidge; | |
| for(let c=0;c<n;c++){ | |
| const pn=randDir(rng);const width=0.004+rng()*0.012;const reddish=rng()<0.6; | |
| const cyc=dr>0.3?0.04*Math.sin:null; // cycloidal modulation flag | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y); | |
| let s=Math.asin(clamp(d[0]*pn[0]+d[1]*pn[1]+d[2]*pn[2],-1,1)); | |
| if(cyc)s-=0.03*dr*Math.sin(Math.atan2(d[0],d[2])*8); // scalloped cycloid path | |
| const dist=Math.abs(s); | |
| if(dist<width*2){const i=y*W+x; | |
| if(dr>0.2){ // raised double ridge: two flanks + central groove | |
| const u=dist/width;Hf[i]+=(Math.exp(-((u-0.7)*(u-0.7))*6)-0.6*Math.exp(-u*u*9))*0.02*dr; | |
| }else if(dist<width)Hf[i]-=0.01*(1-dist/width); | |
| if(dist<width){Tg[i]=4;Ab[i]*=reddish?0.82:1.1;if(reddish)Rb[i]-=0.12*(1-dist/width);} | |
| }} | |
| } | |
| } | |
| function stampTigerStripes(rng){ | |
| const base=randDir(rng); | |
| for(let s=0;s<4;s++){const off=(s-1.5)*0.06; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y);if(d[1]>-0.3)continue; | |
| const dist=Math.abs(Math.asin(clamp(d[0]*base[0]+d[1]*base[1]+d[2]*base[2],-1,1))-off); | |
| if(dist<0.01){const i=y*W+x;Hf[i]-=0.02;Tg[i]=4;Ab[i]*=0.7;Rb[i]-=0.2;}}} | |
| } | |
| function stampSulci(rng){ // Ganymede grooved-terrain swaths: bands of parallel grooves | |
| const a=S.sulci; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| const region=fbm(d[0]*2.5,d[1]*2.5,d[2]*2.5,3,2,.5); | |
| if(region>0.55-0.25*a){const ang=fbm(d[0]*1.5,d[1]*1.5,d[2]*1.5,2,2,.5)*6; | |
| const groove=Math.sin((d[0]*Math.cos(ang)+d[2]*Math.sin(ang))*90+d[1]*40); | |
| Hf[i]+=groove*0.01*a;Ab[i]*=1+groove*0.06*a;}} | |
| } | |
| function applyChaos(rng){ | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| const m=fbm(d[0]*6,d[1]*6,d[2]*6,3,2.3,.5); | |
| if(m>0.25*(1.2-S.chaos))Hf[i]+=(Math.round(m*8)/8-m)*0.15;} | |
| } | |
| function nitrogenGlacier(){ // Pluto Sputnik Planitia: bright flat fill + convection polygons | |
| const a=S.nitrogenGlacier,level=0.34; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x; | |
| if(Hf[i]<level){const d=dirOf(x,y); | |
| Hf[i]=level-(level-Hf[i])*0.05;Tg[i]=7;Ab[i]*=1.25; | |
| // Voronoi-ish polygon ridges | |
| const cell=ridged(d[0]*22,d[1]*22,d[2]*22,2,2,.5); | |
| if(cell>0.78)Hf[i]+=(cell-0.78)*0.04*a;}} | |
| } | |
| function sublimation(){ // scalloped pits + bladed terrain on equator-facing ice | |
| const a=S.sublimation; | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI;const eq=clamp(1-Math.abs(lat)*1.4,0,1); | |
| for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| const pit=fbm(d[0]*30,d[1]*30,d[2]*30,2,2.4,.5); | |
| if(pit>0.2)Hf[i]-=(pit-0.2)*0.03*a*eq; | |
| const blade=Math.abs(Math.sin(d[1]*120));Hf[i]+=blade*0.006*a*eq*smooth(0.4,0.6,Hf[i]);}} | |
| } | |
| function equatorRidge(){ // Iapetus / Vesta equatorial mountain belt | |
| const a=S.equatorRidge; | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI;const b=Math.exp(-(lat*lat)/(0.01)); | |
| if(b<0.02)continue;for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| Hf[i]+=b*0.12*a*(0.7+0.3*fbm(d[0]*8,d[1]*8,d[2]*8,2,2,.5));}} | |
| } | |
| /* ---- flow-routing hydraulic erosion → dendritic rivers, deltas, lakebeds ---- */ | |
| function flowErode(str){ | |
| const N=W*H, order=new Int32Array(N); | |
| for(let i=0;i<N;i++)order[i]=i; | |
| order.sort((a,b)=>Hf[b]-Hf[a]); // process high → low | |
| const acc=new Float32Array(N), down=new Int32Array(N);down.fill(-1); | |
| for(let k=0;k<N;k++){const i=order[k],x=i%W,y=(i/W)|0;acc[i]+=1; | |
| let best=-1,bh=Hf[i]; | |
| for(let dy=-1;dy<=1;dy++){const ny=y+dy;if(ny<0||ny>=H)continue; | |
| for(let dx=-1;dx<=1;dx++){if(!dx&&!dy)continue;const nx=(x+dx+W)%W,j=ny*W+nx; | |
| if(Hf[j]<bh){bh=Hf[j];best=j;}}} | |
| down[i]=best;if(best>=0)acc[best]+=acc[i]; | |
| } | |
| const sea=S.sea; | |
| for(let i=0;i<N;i++){const a=acc[i]; | |
| Hf[i]-=Math.min(str*0.02*Math.pow(a,0.45),0.07); // stream-power incision | |
| if(a>40){ | |
| if(down[i]>=0&&Hf[down[i]]<sea&&sea>0)Ab[i]*=1.35; // delta fan at river mouth | |
| else if(down[i]<0){Hf[i]+=0.004;Ab[i]*=1.18;} // closed-basin lakebed (bright playa) | |
| else if(Tg[i]!==1&&Tg[i]!==6){Ab[i]*=0.78;if(!Tg[i])Tg[i]=8;} // incised channel (tag 8) | |
| } | |
| } | |
| normField(); | |
| } | |
| function massWaste(str){ | |
| for(let y=1;y<H-1;y++){const yo=y*W; | |
| for(let x=0;x<W;x++){const i=yo+x,dn=i+W,l=yo+((x-1+W)%W),r=yo+((x+1)%W); | |
| const grad=Math.max(Math.abs(Hf[r]-Hf[l]),Math.abs(Hf[dn]-Hf[i])); | |
| if(grad>0.03){const mv=grad*str*0.3,tgt=Hf[dn]<Hf[i]?dn:i;Hf[i]-=mv*0.5;Hf[tgt]+=mv*0.5;Ab[i]*=1.04;}}} | |
| } | |
| function riftGrabens(rng){ | |
| const n=Math.round(S.rifts*9); | |
| for(let c=0;c<n;c++){const pn=randDir(rng),off=(rng()-0.5)*1.0,w=0.02+rng()*0.02,depth=0.05*S.rifts; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y); | |
| const s=Math.asin(clamp(d[0]*pn[0]+d[1]*pn[1]+d[2]*pn[2],-1,1))-off*0.3,as=Math.abs(s); | |
| const i=y*W+x; | |
| if(as<w)Hf[i]-=depth*(1-(as/w)*(as/w)); // flat-floored trough with steep walls | |
| else if(as<w*1.5)Hf[i]+=depth*0.18;}} // rift shoulders | |
| } | |
| function coronae(rng){ | |
| const n=Math.round(S.coronae*14); | |
| for(let c=0;c<n;c++){const cd=randDir(rng),rad=0.05+rng()*0.07; | |
| radialStamp(cd,rad,1.3,(i,t)=>{Hf[i]+=0.04*Math.exp(-t*t*2)*S.coronae; | |
| Hf[i]-=Math.exp(-((t-1)/0.12)*((t-1)/0.12))*0.02*S.coronae;});} // central rise + fracture moat | |
| } | |
| function tesserae(){ | |
| const a=S.tesserae; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| if(fbm(d[0]*2+30,d[1]*2,d[2]*2+30,3,2,.5)>0.5-0.3*a&&Hf[i]>0.45){ | |
| const r1=ridged(d[0]*14,d[1]*14,d[2]*14,2,2,.5),r2=ridged(d[2]*14+5,d[0]*14,d[1]*14+5,2,2,.5); | |
| Hf[i]+=(r1+r2-1)*0.03*a;}} // crosshatched two-axis deformation (Venus highlands) | |
| } | |
| function windStreaks(){ | |
| const a=S.windStreaks; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| let ob=0;for(const dd of[0.02,0.05,0.09,0.14]){const j=texelOf([d[0]-WIND[0]*dd,d[1]-WIND[1]*dd,d[2]-WIND[2]*dd]); | |
| ob=Math.max(ob,Hf[j]-Hf[i]-dd*0.5);} | |
| if(ob>0.01)Ab[i]*=1-a*0.22*clamp(ob*6,0,1);} // dark deposition tail downwind of obstacles | |
| } | |
| function magSwirls(){ | |
| const a=S.swirls; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| const wp=fbm(d[0]*2,d[1]*2,d[2]*2,3,2,.5)*3; | |
| const s=ridged(d[0]*4+Math.cos(wp),d[1]*4,d[2]*4+Math.sin(wp),3,2.2,.5); | |
| if(s>0.6)Ab[i]*=1+a*(s-0.6)*1.6;} // bright sinuous swirls (Reiner Gamma) | |
| } | |
| function sharpProvinces(rng){ | |
| const a=S.provinces,n=8+((a*16)|0),pts=[]; | |
| for(let k=0;k<n;k++)pts.push({d:randDir(rng),v:0.7+rng()*0.55}); | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| let best=-9,bv=1;for(const p of pts){const dot=d[0]*p.d[0]+d[1]*p.d[1]+d[2]*p.d[2];if(dot>best){best=dot;bv=p.v;}} | |
| Ab[i]*=lerp(1,bv,a);} // Voronoi compositional units (sharp edges) | |
| } | |
| function faculae(rng){ | |
| const n=Math.round(S.faculae*28); | |
| for(let c=0;c<n;c++){const cd=randDir(rng),rad=0.008+rng()*0.018; | |
| radialStamp(cd,rad,1.0,(i,t)=>{Ab[i]*=1+S.faculae*1.6*(1-t);});} // bright salt spots (Occator) | |
| } | |
| function cryoDomes(rng){ | |
| const n=Math.round(S.cryoDome*16); | |
| for(let c=0;c<n;c++){const cd=randDir(rng),rad=0.02+rng()*0.03,h=0.04+rng()*0.04; | |
| radialStamp(cd,rad,1.4,(i,t)=>{Hf[i]+=h*Math.exp(-t*t*2.2);Ab[i]*=1+0.2*Math.exp(-t*t*2);});} | |
| } | |
| function cantaloupe(){ | |
| const a=S.cantaloupe; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| Hf[i]-=(1-ridged(d[0]*16,d[1]*16,d[2]*16,2,2,.5))*0.025*a;} // dimpled cells (Triton) | |
| } | |
| function wispyTerrain(rng){ | |
| const n=Math.round(S.wispy*30); | |
| for(let c=0;c<n;c++){const pn=randDir(rng),w=0.003+rng()*0.005; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y); | |
| const s=Math.abs(Math.asin(clamp(d[0]*pn[0]+d[1]*pn[1]+d[2]*pn[2],-1,1))); | |
| if(s<w){const i=y*W+x;Ab[i]*=1+S.wispy*0.4*(1-s/w);}}} // bright braided fracture cliffs (Dione) | |
| } | |
| function polygonCracks(){ | |
| const a=S.polygons; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]!==3&&Tg[i]!==7&&Hf[i]>0.5)continue; | |
| const d=dirOf(x,y),c=ridged(d[0]*40,d[1]*40,d[2]*40,2,2,.5); | |
| if(c>0.8){Hf[i]-=(c-0.8)*0.02*a;Ab[i]*=1-0.1*a;}} // thermal-contraction polygons | |
| } | |
| function yardangs(){ | |
| const a=S.yardangs; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Hf[i]<0.35||Hf[i]>0.6)continue; | |
| const d=dirOf(x,y),g=Math.abs(Math.sin((d[0]*WIND[2]-d[2]*WIND[0])*60)); | |
| Hf[i]-=g*0.01*a;} // wind-fluted ridges (Medusae Fossae) | |
| } | |
| function pldSpiral(){ | |
| const a=S.pldSpiral; | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI;if(Math.abs(lat)<1.0)continue; | |
| const r=Math.PI/2-Math.abs(lat); | |
| for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]!==3)continue;const lon=(x/W)*2*Math.PI; | |
| if(Math.sin(lon+r*40)>0.5){Hf[i]-=0.006*a;Ab[i]*=0.95;}}} // spiral troughs in polar layered deposits | |
| } | |
| /* ---- pass-3: regional structure, glacial, coastal, freeze-state ---- */ | |
| function plumeSwell(rng){ | |
| const n=Math.round(S.plumeSwell*5); | |
| for(let c=0;c<n;c++){const cd=randDir(rng),rad=0.3+rng()*0.25,amp=0.12*S.plumeSwell; | |
| radialStamp(cd,rad,1.0,(i,t)=>{Hf[i]+=amp*Math.exp(-t*t*1.4);});} // broad regional uplift (Tharsis) | |
| normField(); | |
| } | |
| function volcanicPlains(){ | |
| const a=S.volcanicPlains; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| if(fbm(d[0]*2.5+12,d[1]*2.5,d[2]*2.5+12,3,2,.5)>0.55-0.2*a){ | |
| Hf[i]=lerp(Hf[i],0.6,0.8*a);Tg[i]=2;Ab[i]*=0.7;}} // constructional positive basalt plateau | |
| } | |
| function glacialErode(str){ | |
| for(let y=1;y<H-1;y++){const lat=Math.PI/2-(y/H)*Math.PI,cold=clamp(Math.abs(Math.sin(lat))*1.4-0.3,0,1);if(cold<0.05)continue; | |
| const yo=y*W;for(let x=0;x<W;x++){const i=yo+x,l=yo+((x-1+W)%W),r=yo+((x+1)%W),u=i-W,dn=i+W; | |
| const avg=(Hf[l]+Hf[r]+Hf[u]+Hf[dn])*0.25; | |
| Hf[i]=lerp(Hf[i],Math.min(avg,Hf[i]+0.02),str*cold*0.5);}} // U-valley floor flattening (glacial) | |
| } | |
| function coastlines(){ | |
| const sea=S.sea,w=0.012*S.coastlines;if(sea<=0)return; | |
| for(let i=0;i<Hf.length;i++){const dh=Hf[i]-sea; | |
| if(dh>0&&dh<w)Hf[i]=sea+dh*0.3; // wave-cut bench | |
| else if(dh>=w&&dh<w*2)Hf[i]+=w*0.4;} // backing sea-cliff | |
| } | |
| function invertedRelief(){ | |
| const a=S.inverted; | |
| for(let i=0;i<Hf.length;i++){if(Tg[i]===8)Hf[i]+=0.02*a;else if(!Tg[i])Hf[i]-=0.01*a;} // channels stand proud | |
| normField(); | |
| } | |
| function degradeCraters(a){ | |
| const tmp=Float32Array.from(Hf); | |
| for(let y=1;y<H-1;y++){const yo=y*W;for(let x=0;x<W;x++){const i=yo+x,l=yo+((x-1+W)%W),r=yo+((x+1)%W),u=i-W,dn=i+W; | |
| Hf[i]=lerp(tmp[i],(tmp[l]+tmp[r]+tmp[u]+tmp[dn])*0.25,a*0.4);}} // age-softened crater rims | |
| } | |
| function glacialFlow(){ | |
| const a=S.glacialFlow; | |
| for(let it=0;it<3;it++)for(let y=1;y<H-1;y++){const yo=y*W;for(let x=0;x<W;x++){const i=yo+x;if(Tg[i]!==7)continue; | |
| const dn=i+W,r=yo+((x+1)%W),l=yo+((x-1+W)%W),u=i-W; | |
| for(const j of[dn,u,l,r])if(Hf[j]<Hf[i]-0.005&&Tg[j]!==7){const mv=(Hf[i]-Hf[j])*0.3*a;Hf[j]+=mv;Tg[j]=7;Ab[j]*=1.2;}}} // lobate N2 tongues | |
| } | |
| function frozenSea(){ | |
| const a=S.frozenSea; | |
| for(let y=0;y<H;y++){const cold=Math.abs(Math.sin(Math.PI/2-(y/H)*Math.PI)); | |
| for(let x=0;x<W;x++){const i=y*W+x;if((Tg[i]===1||Tg[i]===6)&&cold>1-a)Tg[i]=3;}} // freeze a lid on cold seas | |
| } | |
| function seaIce(){ | |
| const a=S.seaIce; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]!==3)continue;const d=dirOf(x,y); | |
| if(ridged(d[0]*30,d[1]*30,d[2]*30,2,2,.5)>0.82-0.1*a)Tg[i]=S.hydrocarbon>0?6:1;} // dark leads in pack ice | |
| } | |
| function spiders(rng){ | |
| const n=Math.round(S.spiders*40); | |
| for(let c=0;c<n;c++){const cd=randDir(rng);if(Math.abs(cd[1])<0.6)continue; | |
| radialStamp(cd,0.02,1.0,(i,t)=>{const x=i%W,y=(i/W)|0,d=dirOf(x,y); | |
| const az=Math.atan2(d[0]-cd[0],d[2]-cd[2]); | |
| if(Math.pow(Math.abs(Math.sin(az*4)),6)>0.3){Hf[i]-=0.004*(1-t)*S.spiders;Ab[i]*=1-0.3*S.spiders*(1-t);}});} // araneiform CO2 spiders | |
| } | |
| /* generic radial stamp helper (great-circle distance kernel) */ | |
| function radialStamp(cd,rad,reach,fn){ | |
| const px=((Math.atan2(cd[0],cd[2])+Math.PI)/(2*Math.PI))*W,py=((Math.PI/2-Math.asin(cd[1]))/Math.PI)*H; | |
| const span=Math.ceil(rad*reach/Math.PI*H)+2; | |
| for(let dy=-span;dy<=span;dy++){let yy=(py+dy)|0;if(yy<0||yy>=H)continue; | |
| const latw=Math.max(0.15,Math.cos(Math.PI/2-(yy/H)*Math.PI)),xs=Math.ceil(span/latw)+1; | |
| for(let dx=-xs;dx<=xs;dx++){let xx=((px+dx)%W+W)%W|0;const d=dirOf(xx,yy); | |
| const t=Math.acos(clamp(d[0]*cd[0]+d[1]*cd[1]+d[2]*cd[2],-1,1))/rad; | |
| if(t<reach)fn(yy*W+xx,t);}} | |
| } | |
| function stampCyclone(cd,rad,spin){ | |
| const e0=[cd[1],cd[2],cd[0]];const e=nrm([cd[1]*e0[2]-cd[2]*e0[1],cd[2]*e0[0]-cd[0]*e0[2],cd[0]*e0[1]-cd[1]*e0[0]]); | |
| const f=nrm([cd[1]*e[2]-cd[2]*e[1],cd[2]*e[0]-cd[0]*e[2],cd[0]*e[1]-cd[1]*e[0]]); | |
| radialStamp(cd,rad,1.6,(i,t)=>{const x=i%W,y=(i/W)|0,d=dirOf(x,y); | |
| const az=Math.atan2(d[0]*f[0]+d[1]*f[1]+d[2]*f[2],d[0]*e[0]+d[1]*e[1]+d[2]*e[2]); | |
| const spiral=0.5+0.5*Math.sin(az+spin*t*7);const eye=smooth(0,0.18,t); | |
| Cl[i]=Math.max(Cl[i],spiral*Math.exp(-t*t*0.7)*eye);}); | |
| } | |
| function tagOceanIce(){ | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI,polar=Math.abs(Math.sin(lat)); | |
| for(let x=0;x<W;x++){const i=y*W+x,d=dirOf(x,y); | |
| const effSea=S.hydrocarbon>0?S.sea*(0.2+0.8*polar):S.sea; // hydrocarbon lakes cluster at the poles (Titan) | |
| if(S.sea>0&&Hf[i]<effSea&&Tg[i]!==2){Tg[i]=S.hydrocarbon>0?6:1;} | |
| const edge=S.iceCap*1.1-0.12*fbm(d[0]*4,d[1]*4,d[2]*4,3,2,.5); | |
| if(S.iceCap>0&&polar>(1-edge)&&Tg[i]!==1&&Tg[i]!==6){Tg[i]=3;Hf[i]=Math.max(Hf[i],S.sea+0.02)+0.01;}}} | |
| } | |
| /* irregular figure: triaxial + lobes + low-freq radial noise → small bodies */ | |
| function buildShape(rng){ | |
| const a=S.irregular;const axB=1-0.3*a,axC=1-0.45*a*(1-S.lobes*0.3); | |
| const lobeDir=randDir(rng),lobeDir2=randDir(rng); | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| let r=1/Math.sqrt(d[0]*d[0]+(d[1]/axC)*(d[1]/axC)+(d[2]/axB)*(d[2]/axB)); // ellipsoid | |
| r*=1+0.18*a*fbm(d[0]*2.2,d[1]*2.2,d[2]*2.2,4,2.1,.55); | |
| if(S.lobes>0){ // contact-binary bulges | |
| const l1=Math.max(0,d[0]*lobeDir[0]+d[1]*lobeDir[1]+d[2]*lobeDir[2]); | |
| const l2=Math.max(0,d[0]*lobeDir2[0]+d[1]*lobeDir2[1]+d[2]*lobeDir2[2]); | |
| r*=1+S.lobes*0.25*(Math.pow(l1,3)+Math.pow(l2,3))-S.lobes*0.12; | |
| } | |
| Shp[i]=r;} | |
| } | |
| function buildClouds(rng){ | |
| Cl.fill(0);Cl2.fill(0); | |
| if(S.clouds<=0&&S.cyclones<=0)return; | |
| if(S.clouds>0)for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| Cl[i]=smooth(0.45-0.4*S.clouds,0.75-0.2*S.clouds,fbm(d[0]*3+20,d[1]*3,d[2]*3+20,5,2.2,.55)); | |
| if(S.multiDeck>0)Cl2[i]=smooth(0.5-0.3*S.clouds,0.82,fbm(d[0]*5+50,d[1]*5,d[2]*5+50,4,2.3,.5)); // lower deck | |
| } | |
| if(S.cyclones>0){const n=Math.round(S.cyclones*8); // spiral storm systems (hurricanes/fronts) | |
| for(let c=0;c<n;c++)stampCyclone(randDir(rng),0.08+rng()*0.10,rng()<0.5?1:-1);} | |
| } | |
| /* gas giants: zonal jets + altitude-color + vortices + hexagon */ | |
| function buildGas(rng){ | |
| const bands=S.bands,warpA=S.bandWarp,irr=S.bandIrregular; | |
| // build an irregular jet-latitude lookup so bands aren't evenly spaced/symmetric | |
| const jets=[];let acc=0; | |
| for(let b=0;b<bands*2+2;b++){acc+=0.6+irr*(rng()-0.5)*1.1;jets.push(acc);} | |
| const jmax=acc; | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI; | |
| let bandV=0.5+0.5*Math.sin((Math.sin(lat)*0.5+0.5)*jmax*Math.PI*2/(bands)); // base | |
| for(let x=0;x<W;x++){const d=dirOf(x,y),i=y*W+x; | |
| const warp=fbm(d[0]*2.2,d[1]*0.7,d[2]*2.2,5,2.1,.55)*warpA; | |
| const turb=fbm(d[0]*7,d[1]*2,d[2]*7,4,2.3,.5)*0.25; | |
| let band=0.5+0.5*Math.sin(lat*bands*2+warp*6); | |
| band=0.5+(band-0.5)*(0.4+0.6*S.bandContrast); // contrast (Uranus≈flat) | |
| // Saturn hexagon: 6-fold perturbation at high north latitude | |
| if(S.hexagon>0&&lat>0.9){const hx=Math.cos(Math.atan2(d[0],d[2])*6)*0.5+0.5;band=lerp(band,hx,S.hexagon*smooth(0.9,1.3,lat));} | |
| Hf[i]=band*0.7+turb+0.3;Ab[i]=1; | |
| // altitude-coded color: bright = high deck | |
| if(S.altColor>0)Ab[i]*=lerp(1,0.7+band*0.6,S.altColor); | |
| } | |
| } | |
| const ns=Math.round(S.storms*22); | |
| for(let c=0;c<ns;c++)stampStorm(rng,0.02+rng()*0.04,rng()<0.5?1.25:0.8,rng()); | |
| if(S.redspot) stampStorm(rng,0.075,1.55,0.0,true,-0.32); | |
| if(S.darkspot)stampStorm(rng,0.06,0.45,0.0,false,0.3); | |
| if(S.polarVortex>0){ // Juno-style polar cyclone clusters | |
| for(const pole of[1,-1]){const ring=5,r=0.16; | |
| for(let k=0;k<ring;k++){const a=k/ring*2*Math.PI; | |
| const lat=pole*1.25,lon=a; | |
| const cd=[Math.cos(lat)*Math.sin(lon),Math.sin(lat),Math.cos(lat)*Math.cos(lon)]; | |
| stampStormDir(cd,0.05,1.3,false);} | |
| stampStormDir([0,pole,0],0.05,1.3,false);} | |
| } | |
| normField(); | |
| } | |
| function stampStorm(rng,rad,bright,seed,red,fixLat){ | |
| const lat=fixLat!==undefined?fixLat*Math.PI/2:(rng()-0.5)*2.2,lon=(seed||rng())*2*Math.PI-Math.PI; | |
| stampStormDir([Math.cos(lat)*Math.sin(lon),Math.sin(lat),Math.cos(lat)*Math.cos(lon)],rad,bright,red); | |
| } | |
| function stampStormDir(cd,rad,bright,red){ | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const d=dirOf(x,y); | |
| const t=Math.acos(clamp(d[0]*cd[0]+d[1]*cd[1]+d[2]*cd[2],-1,1))/rad; | |
| if(t<1.6){const i=y*W+x,sw=Math.exp(-t*t*1.3); | |
| Hf[i]+=sw*0.25*(bright-1);Ab[i]*=lerp(1,bright,sw); | |
| if(red)Rb[i]+=sw*0.6;else if(bright<1)Rb[i]-=sw*0.4;}} | |
| } | |
| function computeNormals(isGas){ | |
| const relief=S.relief*(isGas?0.6:1.0); | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI,cl=Math.max(0.05,Math.cos(lat)); | |
| const yo=y*W,up=(y>0?yo-W:yo),dn=(y<H-1?yo+W:yo); | |
| for(let x=0;x<W;x++){const l=yo+((x-1+W)%W),r=yo+((x+1)%W),i=yo+x; | |
| const dhx=(Hf[r]-Hf[l])/cl*relief*8,dhy=(Hf[dn]-Hf[up])*relief*8; | |
| let nx=-dhx,ny=-dhy,nz=1;const inv=1/Math.hypot(nx,ny,nz); | |
| Nx[i]=nx*inv;Ny[i]=ny*inv;Nz[i]=nz*inv;}} | |
| } | |
| function computeColor(isGas){ | |
| const pal=PALETTES[S.palette]||PALETTES.mars; | |
| for(let y=0;y<H;y++){const lat=Math.PI/2-(y/H)*Math.PI,polar=Math.abs(Math.sin(lat)); | |
| for(let x=0;x<W;x++){const i=y*W+x,d=dirOf(x,y); | |
| let c; | |
| if(isGas){c=rampColor(pal,Hf[i]); | |
| if(S.bandContrast<0.2)c=[lerp(c[0],pal[1][1],0.5),lerp(c[1],pal[1][2],0.5),lerp(c[2],pal[1][3],0.5)]; | |
| // polar hood darkening | |
| const hood=smooth(0.8,1.4,Math.abs(lat))*0.3;c=[c[0]*(1-hood),c[1]*(1-hood),c[2]*(1-hood*0.7)]; | |
| }else{ | |
| c=rampColor(pal,Hf[i]);const tg=Tg[i]; | |
| if(tg===1){const dep=clamp((S.sea-Hf[i])*4,0,1);c=[lerp(40,8,dep),lerp(95,30,dep),lerp(150,70,dep)];} | |
| else if(tg===6){const dep=clamp((S.sea-Hf[i])*4,0,1);c=[lerp(35,12,dep),lerp(28,8,dep),lerp(20,6,dep)];} // hydrocarbon | |
| else if(tg===2){c=S.sulfur?[c[0],c[1]*0.8,c[2]*0.4]:[c[0]*0.5,c[1]*0.5,c[2]*0.55];} | |
| else if(tg===3){c=[235,240,248];} | |
| else if(tg===7){c=[lerp(c[0],235,0.6),lerp(c[1],240,0.6),lerp(c[2],245,0.6)];} // N2 glacier | |
| else if(tg===4){c=[c[0]*0.7+60,c[1]*0.6+25,c[2]*0.6+20];} | |
| else if(tg===8){c=S.hydrocarbon>0?[c[0]*0.45,c[1]*0.4,c[2]*0.4]:[c[0]*0.6,c[1]*0.66,c[2]*0.82];} // river channel | |
| // relief-free albedo provinces | |
| if(S.albedoProv>0){const m=fbm(d[0]*2.3+7,d[1]*2.3,d[2]*2.3+7,4,2.1,.55); | |
| const f=1+m*0.3*S.albedoProv;c=[c[0]*f,c[1]*f,c[2]*f];} | |
| // hemispheric albedo dichotomy (Iapetus): leading dark / trailing bright | |
| if(S.hemiAlbedo>0){const dot=d[0]*HEMI[0]+d[1]*HEMI[1]+d[2]*HEMI[2]; | |
| const f=lerp(1,dot>0?0.35:1.25,S.hemiAlbedo);c=[c[0]*f,c[1]*f,c[2]*f];} | |
| // slope streaks: downslope dark albedo lineae (relief-free color) | |
| if(S.slopeStreaks>0){const slope=1-Nz[i];if(slope>0.25){ | |
| const st=fbm(d[0]*30,d[1]*60,d[2]*30,2,2,.5);if(st>0.1){const f=1-0.35*S.slopeStreaks*slope;c=[c[0]*f,c[1]*f,c[2]*f];}}} | |
| if(S.latBands>0){const lb=1+0.25*S.latBands*Math.sin(polar*9);c=[c[0]*lb,c[1]*lb,c[2]*lb];} // zonal albedo belts | |
| if(S.devilTracks>0&&ridged(d[0]*38+3,d[1]*38,d[2]*38+3,2,2.2,.5)>0.76){const f=1-0.22*S.devilTracks;c=[c[0]*f,c[1]*f,c[2]*f];} // dust-devil tracks | |
| // seasonal frost: faint bright halo beyond permanent cap | |
| if(S.seasonalFrost>0&&polar>(1-S.iceCap*1.1-S.seasonalFrost*0.4)&&tg!==3){ | |
| const f=smooth(1-S.iceCap*1.1-S.seasonalFrost*0.4,1-S.iceCap*1.1,polar); | |
| c=[lerp(c[0],235,f*0.7),lerp(c[1],240,f*0.7),lerp(c[2],248,f*0.7)];} | |
| // chemistry tints | |
| if(S.oxidation>0){const ox=S.oxidation*0.55;c=[lerp(c[0],c[0]*1.15+40,ox),lerp(c[1],c[1]*0.8,ox),lerp(c[2],c[2]*0.6,ox)];} | |
| if(S.tholin>0){const th=S.tholin*0.4;c=[lerp(c[0],200,th),lerp(c[1],130,th),lerp(c[2],70,th)];} | |
| if(S.polarTholin>0){const pt=S.polarTholin*smooth(0.55,0.95,polar);c=[lerp(c[0],150,pt),lerp(c[1],70,pt),lerp(c[2],55,pt)];} | |
| if(S.weather>0){const w=S.weather*0.5;c=[c[0]*(1-w*0.45),c[1]*(1-w*0.55),c[2]*(1-w*0.6)];} | |
| } | |
| const a=Ab[i],rb=Rb[i]*120; | |
| colR[i]=clamp(c[0]*a+rb,0,255);colG[i]=clamp(c[1]*a+rb,0,255);colB[i]=clamp(c[2]*a+rb,0,255); | |
| } | |
| } | |
| } | |
| function buildEmissive(){ | |
| if(S.cityLights<=0)return; | |
| for(let y=0;y<H;y++)for(let x=0;x<W;x++){const i=y*W+x;if(Tg[i]===1||Tg[i]===6||Tg[i]===3)continue; | |
| const d=dirOf(x,y);const m=fbm(d[0]*9,d[1]*9,d[2]*9,3,2.2,.5); | |
| if(m>0.45){En[i]=clamp((m-0.45)*4,0,1)*S.cityLights;}} | |
| } | |
| /* ================= RENDER ================= */ | |
| const view=document.getElementById("view"),vctx=view.getContext("2d"); | |
| let VW=view.width,VH=view.height,img=vctx.createImageData(VW,VH); | |
| let yaw=0.4,pitch=0.25,zoom=0.74,autoSpin=true; | |
| let starBuf=null; | |
| function makeStars(){starBuf=new Uint8ClampedArray(VW*VH*3);const r=mulberry32(99); | |
| for(let i=0;i<VW*VH;i++){starBuf[i*3]=5;starBuf[i*3+1]=6;starBuf[i*3+2]=10;} | |
| for(let s=0;s<VW*VH/650;s++){const x=(r()*VW)|0,y=(r()*VH)|0,b=120+r()*135,i=(y*VW+x)*3; | |
| starBuf[i]=b;starBuf[i+1]=b;starBuf[i+2]=b*1.05;}} | |
| makeStars(); | |
| function render(){ | |
| const cx=VW/2,cy=VH/2,R=Math.min(VW,VH)/2*zoom; | |
| // model->view = Rx(pitch) · Rz(axialTilt) · Ry(yaw) — obliquity tilts the spin axis (Uranus on its side) | |
| const ty=yaw,tz=(S.axialTilt||0)*Math.PI/180,tx=pitch; | |
| const cyA=Math.cos(ty),syA=Math.sin(ty),czA=Math.cos(tz),szA=Math.sin(tz),cxA=Math.cos(tx),sxA=Math.sin(tx); | |
| const M=mm3([[1,0,0],[0,cxA,-sxA],[0,sxA,cxA]],mm3([[czA,-szA,0],[szA,czA,0],[0,0,1]],[[cyA,0,syA],[0,1,0],[-syA,0,cyA]])); | |
| const r00=M[0][0],r01=M[0][1],r02=M[0][2],r10=M[1][0],r11=M[1][1],r12=M[1][2],r20=M[2][0],r21=M[2][1],r22=M[2][2]; | |
| // sun in view space | |
| const az=S.sunAz*Math.PI/180,el=S.sunEl*Math.PI/180; | |
| const sx=Math.sin(az)*Math.cos(el),sy=Math.sin(el),sz=Math.cos(az)*Math.cos(el); | |
| const SUN=nrm([sx,sy,sz]); | |
| // sun in model space (Rᵀ·SUN) | |
| const smx=r00*SUN[0]+r10*SUN[1]+r20*SUN[2],smy=r01*SUN[0]+r11*SUN[1]+r21*SUN[2],smz=r02*SUN[0]+r12*SUN[1]+r22*SUN[2]; | |
| const data=img.data; | |
| const isGas=S.mode==="gas",atmo=S.atmoGlow||0; | |
| const obl=S.oblate,az2=1-obl,irr=S.irregular>0; | |
| const pal=PALETTES[S.palette]||PALETTES.mars; | |
| // body-matched limb glow: warm for warm palettes, blue only where Rayleigh is strong | |
| const gpal=PALETTES[S.palette]||PALETTES.mars,gt=gpal[gpal.length-1]; | |
| let glow=[gt[1],gt[2],gt[3]]; | |
| {const rb=(S.rayleigh||0)*0.7;glow=[lerp(glow[0],150,rb),lerp(glow[1],180,rb),lerp(glow[2],225,rb)];} | |
| const ambient=S.ambient; | |
| // sky/haze tint: shifts blue with Rayleigh, warm-tan otherwise (Titan) | |
| const sky=[lerp(205,150,S.rayleigh||0),lerp(190,178,S.rayleigh||0),lerp(160,215,S.rayleigh||0)]; | |
| // star color temperature: 0=red dwarf, .5=sun-like white, 1=blue | |
| const stt=S.starTemp; | |
| const sunCol=stt<0.5?[1,lerp(0.6,1,stt*2),lerp(0.34,1,stt*2)]:[lerp(1,0.72,(stt-0.5)*2),lerp(1,0.82,(stt-0.5)*2),1]; | |
| // ring geometry (model space equatorial plane y=0) | |
| const hasRing=S.rings>0,rI=S.ringInner,rO=S.ringOuter; | |
| // moons (model-space positions in equatorial plane) | |
| const moons=[]; | |
| if(S.moons>0){const mr=mulberry32(S.seed^0x55).bind(null); | |
| for(let m=0;m<S.moons;m++){const a=(m/Math.max(1,S.moons))*2*Math.PI+1,dd=2.4+m*0.7; | |
| moons.push({p:[Math.cos(a)*dd,0.12*(m-0.5),Math.sin(a)*dd],r:0.14+0.03*m});}} | |
| for(let py=0;py<VH;py++){ | |
| const ny0=(py-cy)/R; | |
| for(let px=0;px<VW;px++){ | |
| const o=(py*VW+px)*4,nx0=(px-cx)/R; | |
| // ---- ring sample (compute first for depth compositing) ---- | |
| let ringHit=false,ringZ=-2,ringR=0,ringG2=0,ringB=0,ringA=0; | |
| if(hasRing){ | |
| // view ray point (nx0,ny0,z); model = Rᵀ·(nx0,ny0,z). plane y_model=0. | |
| // Ay + z*By = 0 → Ay = r01*nx0+r11*ny0 ; By = r21 | |
| const Ay=r01*nx0+r11*ny0, By=r21; | |
| if(Math.abs(By)>1e-4){const z=-Ay/By; | |
| const Ax=r00*nx0+r10*ny0,Bx=r20, Az=r02*nx0+r12*ny0,Bz=r22; | |
| const mx=Ax+z*Bx, mz=Az+z*Bz, rr=Math.hypot(mx,mz); | |
| if(rr>=rI&&rr<=rO){ringHit=true;ringZ=z; | |
| const tt=(rr-rI)/(rO-rI); | |
| // banded structure + Cassini/Encke-style divisions (gaps you can see through) | |
| let dens=0.55+0.45*Math.sin(tt*38)+0.2*Math.sin(tt*140); | |
| dens*=1-0.9*Math.exp(-((tt-0.46)*(tt-0.46))/0.0008); // Cassini Division | |
| dens*=1-0.8*Math.exp(-((tt-0.72)*(tt-0.72))/0.00018); // Encke Gap | |
| dens*=smooth(0,0.05,tt)*smooth(0,0.05,1-tt); // soft inner/outer edge | |
| if(S.ringSpokes>0){const az=Math.atan2(mx,mz);dens*=1-S.ringSpokes*0.6*Math.pow(Math.abs(Math.cos(az*3.5)),6);} // radial spokes | |
| ringA=clamp(dens,0,1)*S.ringOpacity; | |
| const cc=rampColor(pal,0.42+0.45*Math.sin(tt*6)); | |
| // ring lit unless inside planet shadow | |
| let shadow=1;const proj=mx*smx+0+mz*smz; // approx planet-shadow test in plane | |
| const perp=Math.hypot(mx-proj*smx, -proj*smy, mz-proj*smz); | |
| if(proj<0&&perp<1.0)shadow=0.15; | |
| ringR=cc[0]*shadow;ringG2=cc[1]*shadow;ringB=cc[2]*shadow; | |
| if(S.ringForward>0){const fwd=S.ringForward*Math.max(0,-SUN[2])*0.8; // backlit forward-scatter | |
| ringA=clamp(ringA*(1+fwd*2),0,1);ringR+=fwd*120;ringG2+=fwd*120;ringB+=fwd*120;}}} | |
| } | |
| // ---- planet intersection ---- | |
| let hit=false,hz=-2,d0,d1,d2,gnx,gny,gnz; | |
| if(!irr){ | |
| // ellipsoid (axes 1,az2,1). transform: scale y. solve in view via model quadratic. | |
| if(obl<1e-3){ | |
| const r2=nx0*nx0+ny0*ny0; | |
| if(r2<=1){hit=true;const nz=Math.sqrt(1-r2);hz=nz; | |
| d0=r00*nx0+r10*ny0+r20*nz; d1=r01*nx0+r11*ny0+r21*nz; d2=r02*nx0+r12*ny0+r22*nz; | |
| gnx=d0;gny=d1;gnz=d2;} | |
| }else{ | |
| // model ray A + z B ; ellipsoid x²+ (y/az2)² + z²=1 | |
| const Ax=r00*nx0+r10*ny0,Bx=r20, Ay=r01*nx0+r11*ny0,By=r21, Az=r02*nx0+r12*ny0,Bz=r22; | |
| const iy=1/(az2*az2); | |
| const A=Bx*Bx+By*By*iy+Bz*Bz, Bq=2*(Ax*Bx+Ay*By*iy+Az*Bz), C=Ax*Ax+Ay*Ay*iy+Az*Az-1; | |
| const disc=Bq*Bq-4*A*C; | |
| if(disc>=0){const z=(-Bq+Math.sqrt(disc))/(2*A);hit=true;hz=z; | |
| d0=Ax+z*Bx;d1=Ay+z*By;d2=Az+z*Bz; | |
| gnx=d0;gny=d1*iy;gnz=d2;const gl=Math.hypot(gnx,gny,gnz)||1;gnx/=gl;gny/=gl;gnz/=gl;} | |
| } | |
| }else{ | |
| // irregular star-shaped figure: ray-march radius field Shp(dir) | |
| const Ax=r00*nx0+r10*ny0,Bx=r20,Ay=r01*nx0+r11*ny0,By=r21,Az=r02*nx0+r12*ny0,Bz=r22; | |
| const maxR=1.5;const rad2=nx0*nx0+ny0*ny0; | |
| if(rad2<maxR*maxR){ | |
| let zf=Math.sqrt(Math.max(0.0001,maxR*maxR-rad2)),prev=1,found=false,zh=0; | |
| for(let st=0;st<26;st++){const z=zf-st*(2*zf/26); | |
| const mx=Ax+z*Bx,my=Ay+z*By,mz=Az+z*Bz,len=Math.hypot(mx,my,mz); | |
| if(len<0.01)continue;const sh=Shp[texelOf([mx/len,my/len,mz/len])];const f=len-sh; | |
| if(f<=0&&st>0){const z2=zf-(st-1)*(2*zf/26);zh=lerp(z,z2,(0-f)/((prev)-(f)||1));found=true;break;} | |
| prev=f;} | |
| if(found){const mx=Ax+zh*Bx,my=Ay+zh*By,mz=Az+zh*Bz,len=Math.hypot(mx,my,mz)||1; | |
| hit=true;hz=zh;d0=mx/len;d1=my/len;d2=mz/len;gnx=d0;gny=d1;gnz=d2;} | |
| } | |
| } | |
| let rr,gg,bb; | |
| let drewPlanet=false; | |
| if(hit){ | |
| const dl=Math.hypot(d0,d1,d2)||1;const dd0=d0/dl,dd1=d1/dl,dd2=d2/dl; | |
| const ti=texelOf([dd0,dd1,dd2]); | |
| // tangent frame around geometric normal (use sphere dir basis) | |
| let ex=dd2,ey=0,ez=-dd0;let el2=Math.hypot(ex,ey,ez)||1;ex/=el2;ez/=el2; | |
| const nox=dd1*ez-dd2*ey,noy=dd2*ex-dd0*ez,noz=dd0*ey-dd1*ex; | |
| const tnx=Nx[ti],tny=Ny[ti],tnz=Nz[ti]; | |
| // bump normal in model space, blended toward geometric normal for irregular | |
| let wnx=tnx*ex+tny*nox+tnz*gnx, wny=tnx*ey+tny*noy+tnz*gny, wnz=tnx*ez+tny*noz+tnz*gnz; | |
| // fade surface relief toward the smooth geometric normal near the terminator (kills night-side facets) | |
| {const gdk=clamp((gnx*smx+gny*smy+gnz*smz+0.05)/0.28,0,1); | |
| wnx=lerp(gnx,wnx,gdk);wny=lerp(gny,wny,gdk);wnz=lerp(gnz,wnz,gdk); | |
| const wl=Math.hypot(wnx,wny,wnz)||1;wnx/=wl;wny/=wl;wnz/=wl;} | |
| // model-space lighting (sun in model space) — keeps phase stable under spin | |
| let dif=wnx*smx+wny*smy+wnz*smz; | |
| const tw=S.atmThickness||0; // thicker air → wider twilight band | |
| const lit=smooth(-0.05-tw*0.4,0.08+tw*0.45,dif); | |
| let light=ambient+(1-ambient)*Math.max(0,dif)*lit; | |
| // opposition surge (backscatter near zero phase) | |
| if(S.oppSurge>0){const vdot=dd0* (r02*0+ r12*0+ r22*1); /*view z in model*/ | |
| const vm0=r20,vm1=r21,vm2=r22;const vdotn=wnx*vm0+wny*vm1+wnz*vm2; | |
| if(dif>0.2&&vdotn>0.6)light+=S.oppSurge*0.3*Math.pow(vdotn,6);} | |
| // Lommel-Seeliger photometry (flat full-disk regolith, not limb-darkened Lambert) | |
| if(S.lommel>0){const vz=r20*wnx+r21*wny+r22*wnz,mu=Math.max(0.05,vz),mu0=Math.max(0,dif); | |
| light=lerp(light,ambient+(1-ambient)*(mu0/(mu0+mu))*lit,S.lommel);} | |
| // cast shadows: march toward the sun across the heightfield (long shadows at low sun) | |
| if(S.castShadow>0&&dif>0.02){let occ=0; | |
| for(let s=1;s<=8;s++){const st=s*0.018,j=texelOf([dd0+smx*st,dd1+smy*st,dd2+smz*st]); | |
| if(Hf[j]>Hf[ti]+st*dif*6*S.relief)occ++;} | |
| occ=clamp(occ/5,0,1); // fractional → soft penumbra (8 samples) | |
| if(occ>0)light=lerp(light,ambient*0.7,occ);} // shadowed, not pure black (sky fill) | |
| const tg=Tg[ti]; | |
| // specular / sun-glint for liquids & ice | |
| if((tg===1||tg===3||tg===6)&&dif>0){ | |
| const hvx=smx,hvy=smy,hvz=smz+1,hl=Math.hypot(hvx,hvy,hvz)||1; | |
| let spec=(wnx*hvx+wny*hvy+wnz*hvz)/hl;if(spec<0)spec=0; | |
| light+=Math.pow(spec,tg===6?40:28)*(tg===1?0.9:tg===6?1.1:0.5);} | |
| rr=colR[ti]*light;gg=colG[ti]*light;bb=colB[ti]*light; | |
| rr*=sunCol[0];gg*=sunCol[1];bb*=sunCol[2]; // illuminant spectral tint | |
| if(S.earthshine>0){const es=S.earthshine*0.16*Math.max(0,0.25-dif);rr+=es*45;gg+=es*60;bb+=es*85;} | |
| // ---- exoworld / tidal-lock surface physics ---- | |
| if(S.frozen>0||S.lavaWorld>0||S.tidalLock>0){ | |
| const sdot=dd0*smx+dd1*smy+dd2*smz; // 1=substellar, -1=antistellar | |
| if(S.frozen>0){let cold=S.frozen; | |
| if(S.tidalLock>0)cold*=clamp(1-smooth(-0.05,0.55,sdot),0,1); | |
| if(cold>0.01){rr=lerp(rr,228*light,cold);gg=lerp(gg,237*light,cold);bb=lerp(bb,246*light,cold);}} | |
| if(S.tidalLock>0&&S.frozen>0){ // eyeball: substellar meltwater sea | |
| const sea=smooth(0.5,0.85,sdot); | |
| if(sea>0){rr=lerp(rr,16*light,sea);gg=lerp(gg,68*light,sea);bb=lerp(bb,118*light,sea); | |
| const hl=Math.hypot(smx,smy,smz+1)||1;let sp=(wnx*smx+wny*smy+wnz*(smz+1))/hl;if(sp<0)sp=0; | |
| const g=Math.pow(sp,30)*sea;rr+=g*120;gg+=g*130;bb+=g*140;}} | |
| if(S.lavaWorld>0){ // molten rock self-emission (glows on night side) | |
| let molten=S.lavaWorld; | |
| if(S.tidalLock>0)molten*=smooth(-0.05,0.5,sdot); | |
| // crack pattern from height so it reads as molten fissures, not a flat glow | |
| const crack=clamp(0.6+0.8*(Hf[ti]-0.4),0,1);molten*=0.5+0.5*crack; | |
| if(molten>0.01){rr=lerp(rr,22,molten*0.6);gg=lerp(gg,9,molten*0.6);bb=lerp(bb,6,molten*0.6); | |
| const e=molten*molten;rr+=e*240;gg+=e*92;bb+=e*22;}} | |
| } | |
| // ring shadow on planet surface | |
| if(hasRing&&smy!==0){const s=-dd1/smy;if(s>0){const hx=dd0+s*smx,hz2=dd2+s*smz,hr=Math.hypot(hx,hz2); | |
| if(hr>=rI&&hr<=rO)light*=0.45,rr*=0.55,gg*=0.55,bb*=0.55;}} | |
| // moon shadow (transit) on planet | |
| for(const mo of moons){const L=[smx,smy,smz]; | |
| const toM=[mo.p[0]-dd0,mo.p[1]-dd1,mo.p[2]-dd2];const proj=toM[0]*L[0]+toM[1]*L[1]+toM[2]*L[2]; | |
| if(proj>0){const px2=toM[0]-proj*L[0],py2=toM[1]-proj*L[1],pz2=toM[2]-proj*L[2]; | |
| const sh=smooth(mo.r,mo.r*0.5,Math.hypot(px2,py2,pz2)); // umbra→penumbra soft edge | |
| if(sh>0&&dif>0){const f=1-0.7*sh;rr*=f;gg*=f;bb*=f;}}} | |
| // cloud shadows cast onto the surface below | |
| if(S.cloudShadows>0&&S.clouds>0){const cs=Cl[texelOf([dd0+smx*0.05,dd1+smy*0.05,dd2+smz*0.05])]; | |
| if(cs>0.1){const sh=1-cs*S.cloudShadows*0.55;rr*=sh;gg*=sh;bb*=sh;}} | |
| // clouds composite: lower (warm) deck then upper (white) deck → gaps show the deck below | |
| if(S.clouds>0){const clight=ambient+(1-ambient)*Math.max(0,dif); | |
| if(S.multiDeck>0){const cv2=Cl2[ti];if(cv2>0){const a2=cv2*S.cloudOpacity*0.8; | |
| rr=lerp(rr,235*clight,a2);gg=lerp(gg,210*clight,a2);bb=lerp(bb,180*clight,a2);}} | |
| const cv=Cl[ti];if(cv>0){const cc=255*clight,a=cv*S.cloudOpacity; | |
| rr=lerp(rr,cc,a);gg=lerp(gg,cc,a);bb=lerp(bb,cc*1.02,a); | |
| if(S.lightning>0&&dif<0.06&&cv>0.55&&Math.random()<0.0006*S.lightning){rr+=210;gg+=220;bb+=255;}}} | |
| // emissive city lights on night side | |
| if(S.cityLights>0&&dif<0.1){const e=En[ti]*(1-Math.max(0,dif)*10);if(e>0){rr+=e*180;gg+=e*160;bb+=e*90;}} | |
| // aurora: emissive oval around the magnetic poles, brightest on the night side | |
| if(S.aurora>0){const pl=Math.abs(dd1); | |
| const oval=smooth(0.74,0.84,pl)*(1-smooth(0.9,0.99,pl)); | |
| if(oval>0){const flick=0.55+0.45*Math.sin(Hf[ti]*80+dd0*30); | |
| const a=S.aurora*oval*flick*(1-clamp(dif,0,1)*0.7); | |
| rr+=a*55;gg+=a*185;bb+=a*110;}} | |
| // dust storm haze | |
| if(S.dustStorm>0){const k=S.dustStorm*0.6;rr=lerp(rr,200*light+40,k);gg=lerp(gg,150*light+30,k);bb=lerp(bb,90*light+20,k);} | |
| // ---- atmosphere: limb darkening → haze veil → Rayleigh → glow ---- | |
| const limbW=1-Math.abs(hz); | |
| if(S.limbDarkening>0){const ld=1-S.limbDarkening*0.7*Math.pow(limbW,1.4);rr*=ld;gg*=ld;bb*=ld;} | |
| if(S.haze>0){const veil=clamp(S.haze*(0.25+0.75*limbW*limbW)*(1+(S.polarHood||0)*smooth(0.55,0.9,Math.abs(dd1))),0,1); | |
| rr=lerp(rr,sky[0]*(0.4+0.6*light),veil);gg=lerp(gg,sky[1]*(0.4+0.6*light),veil);bb=lerp(bb,sky[2]*(0.4+0.6*light),veil);} | |
| if(S.rayleigh>0){const edge=Math.pow(limbW,3); | |
| const day=edge*Math.max(0,dif)*S.rayleigh;rr+=day*40;gg+=day*95;bb+=day*185; // blue day limb | |
| const twi=Math.exp(-dif*dif*55)*edge*S.rayleigh;rr+=twi*130;gg+=twi*48;bb+=twi*12;} // sunset ring | |
| if(atmo){const limb=Math.pow(limbW,2.0)*atmo*0.4;rr+=glow[0]*limb;gg+=glow[1]*limb;bb+=glow[2]*limb;} | |
| drewPlanet=true; | |
| } | |
| // ---- composite ring + planet + background ---- | |
| if(ringHit&&(!hit||ringZ>hz)){ | |
| // ring in front of planet (or planet missed) | |
| let baseR,baseG,baseB; | |
| if(drewPlanet){baseR=rr;baseG=gg;baseB=bb;} | |
| else{const si=(py*VW+px)*3;baseR=starBuf[si];baseG=starBuf[si+1];baseB=starBuf[si+2];} | |
| rr=lerp(baseR,ringR,ringA);gg=lerp(baseG,ringG2,ringA);bb=lerp(baseB,ringB,ringA); | |
| drewPlanet=true; | |
| }else if(ringHit&&hit&&ringZ<hz){ | |
| // ring behind planet: planet already drawn, ring hidden — nothing | |
| } | |
| if(!drewPlanet){ | |
| const si=(py*VW+px)*3;let br=starBuf[si],bg=starBuf[si+1],bb2=starBuf[si+2]; | |
| const r2=nx0*nx0+ny0*ny0; | |
| const halo=0.1+(S.atmThickness||0)*0.3; // thicker air → taller glow envelope | |
| if(atmo&&!irr&&r2<1+halo+0.04&&r2>0.9){const g=clamp(1-(Math.sqrt(r2)-1)/halo,0,1)*0.35*atmo; | |
| if(g>0){const sun2=Math.max(0,(nx0*SUN[0]+ny0*SUN[1]));br+=glow[0]*g*(0.4+sun2);bg+=glow[1]*g*(0.4+sun2);bb2+=glow[2]*g*(0.4+sun2);}} | |
| // detached aerosol haze layers above the limb (Titan / Pluto) | |
| if(S.hazeLayers>0&&!irr&&r2>1&&r2<1.55){const hh=Math.sqrt(r2)-1; | |
| let layer=0;for(let L=0;L<5;L++){const alt=0.03+L*0.06;layer+=Math.exp(-((hh-alt)*(hh-alt))/0.0008);} | |
| const env=Math.exp(-hh*7),sun2=Math.max(0,(nx0*SUN[0]+ny0*SUN[1])); | |
| const g=(layer*0.4+env*0.3)*S.hazeLayers*(0.35+sun2);br+=sky[0]*g;bg+=sky[1]*g;bb2+=sky[2]*g;} | |
| rr=br;gg=bg;bb=bb2; | |
| } | |
| // ---- comet coma + anti-sunward tail (active small bodies / evaporating planets) ---- | |
| if(S.tail>0){ | |
| const tdx=-SUN[0],tdy=-SUN[1],tl=Math.hypot(tdx,tdy)||1,ux=tdx/tl,uy=tdy/tl; | |
| const along=nx0*ux+ny0*uy, perp=Math.abs(nx0*uy-ny0*ux); | |
| const coma=Math.exp(-(nx0*nx0+ny0*ny0)*3.2)*0.9; | |
| let tail=0; | |
| if(along>0){const wid=0.14+along*0.30;tail=Math.exp(-(perp*perp)/(wid*wid))*Math.exp(-along*0.45)*0.8;} | |
| const g=(coma+tail)*S.tail; | |
| rr+=g*140;gg+=g*170;bb+=g*215; // straight bluish ion tail + white coma | |
| // curved warm dust tail, offset from the ion tail | |
| const ca=Math.cos(0.35),sa=Math.sin(0.35),ux2=ux*ca-uy*sa,uy2=ux*sa+uy*ca; | |
| const al2=nx0*ux2+ny0*uy2,pp2=nx0*uy2-ny0*ux2; | |
| if(al2>0){const w2=0.18+al2*0.4,dust=Math.exp(-(pp2*pp2)/(w2*w2))*Math.exp(-al2*0.4)*0.7*S.tail; | |
| rr+=dust*180;gg+=dust*150;bb+=dust*90;} | |
| } | |
| // ---- moons (lit spheres, drawn over everything if in front) ---- | |
| for(const mo of moons){ | |
| // project moon center to view | |
| const mvx=r00*mo.p[0]+r01*mo.p[1]+r02*mo.p[2]; | |
| const mvy=r10*mo.p[0]+r11*mo.p[1]+r12*mo.p[2]; | |
| const mvz=r20*mo.p[0]+r21*mo.p[1]+r22*mo.p[2]; | |
| const ddx=nx0-mvx,ddy=ny0-mvy;const rm2=ddx*ddx+ddy*ddy; | |
| if(rm2<mo.r*mo.r&&(mvz>hz||!hit)){const nz=Math.sqrt(mo.r*mo.r-rm2)/mo.r; | |
| // moon surface normal view-space | |
| const mnx=ddx/mo.r,mny=ddy/mo.r,mnz=nz; | |
| let dif=mnx*SUN[0]+mny*SUN[1]+mnz*SUN[2];if(dif<0)dif=0; | |
| const l=0.05+0.95*dif;rr=150*l;gg=145*l;bb=140*l;drewPlanet=true;} | |
| } | |
| data[o]=rr;data[o+1]=gg;data[o+2]=bb;data[o+3]=255; | |
| } | |
| } | |
| vctx.putImageData(img,0,0); | |
| } | |
| function loop(){if(autoSpin)yaw+=0.0030;render();requestAnimationFrame(loop);} | |
| /* interaction */ | |
| let drag=false,lx=0,ly=0; | |
| view.addEventListener("pointerdown",e=>{drag=true;lx=e.clientX;ly=e.clientY;view.setPointerCapture(e.pointerId);}); | |
| view.addEventListener("pointermove",e=>{if(!drag)return;yaw+=(e.clientX-lx)*0.006;pitch=clamp(pitch+(e.clientY-ly)*0.006,-1.3,1.3);lx=e.clientX;ly=e.clientY;}); | |
| view.addEventListener("pointerup",()=>drag=false); | |
| view.addEventListener("wheel",e=>{e.preventDefault();zoom=clamp(zoom*(e.deltaY>0?0.93:1.07),0.3,2.4);},{passive:false}); | |
| /* ================= UI ================= */ | |
| const SLIDERS=[ | |
| ["grpFig","oblate","Oblateness (rotational flattening)",0,.3,.005], | |
| ["grpFig","irregular","Irregular figure (small body)",0,1,.01], | |
| ["grpFig","lobes","Contact-binary lobes",0,1,.01], | |
| ["grpFig","axialTilt","Axial tilt / obliquity °",0,120,1], | |
| ["grpFig","sunAz","Sun azimuth °",0,360,1], | |
| ["grpFig","sunEl","Sun elevation °",-60,60,1], | |
| ["grpFig","ambient","Night-side ambient (phase)",0,.3,.005], | |
| ["grpFig","relief","Relief exaggeration",.1,2,.01], | |
| ["grpFig","castShadow","Cast shadows (low-sun terrain)",0,1,.01], | |
| ["grpFig","lommel","Lommel-Seeliger (flat regolith)",0,1,.01], | |
| ["grpFig","starTemp","Star color (red↔blue)",0,1,.01], | |
| ["grpFig","earthshine","Earthshine (night-side fill)",0,1,.01], | |
| ["grpEndo","tectonics","Tectonics / continents",0,1,.01], | |
| ["grpEndo","mountains","Mountain ridges (orogeny)",0,1,.01], | |
| ["grpEndo","scarps","Lobate thrust scarps",0,1,.01], | |
| ["grpEndo","wrinkle","Wrinkle ridges (on plains)",0,1,.01], | |
| ["grpEndo","volcano","Volcanism (shields)",0,1,.01], | |
| ["grpEndo","floods","Flood basalts / maria",0,1,.01], | |
| ["grpEndo","rifts","Rift valleys / grabens",0,1,.01], | |
| ["grpEndo","coronae","Coronae (ring structures)",0,1,.01], | |
| ["grpEndo","tesserae","Tesserae (deformed highlands)",0,1,.01], | |
| ["grpEndo","plumeSwell","Mantle-plume swell (Tharsis rise)",0,1,.01], | |
| ["grpEndo","volcanicPlains","Volcanic plains / plateaus",0,1,.01], | |
| ["grpExo","craters","Impact bombardment",0,1,.01], | |
| ["grpExo","craterSize","Crater scale",0,1,.01], | |
| ["grpExo","relaxation","Viscous relaxation (palimpsests)",0,1,.01], | |
| ["grpExo","multiring","Multi-ring basins",0,1,.01], | |
| ["grpExo","secondaries","Secondary crater chains",0,1,.01], | |
| ["grpExo","erosion","Thermal erosion (diffusion)",0,1,.01], | |
| ["grpExo","rivers","Hydraulic rivers (flow-routing)",0,1,.01], | |
| ["grpExo","masswaste","Mass wasting (landslides)",0,1,.01], | |
| ["grpExo","glacial","Glacial erosion (U-valleys)",0,1,.01], | |
| ["grpExo","coastlines","Coastlines / wave-cut benches",0,1,.01], | |
| ["grpExo","inverted","Inverted relief (deflation)",0,1,.01], | |
| ["grpExo","degradation","Crater degradation (age)",0,1,.01], | |
| ["grpExo","yardangs","Wind erosion / yardangs",0,1,.01], | |
| ["grpExo","dunes","Aeolian dunes",0,1,.01], | |
| ["grpExo","stratify","Sedimentary stratification",0,1,.01], | |
| ["grpExo","weather","Space weathering (age)",0,1,.01], | |
| ["grpCryo","sea","Sea level",0,1,.01], | |
| ["grpCryo","hydrocarbon","Hydrocarbon (vs water) seas",0,1,.01], | |
| ["grpCryo","iceCap","Polar ice caps",0,1,.01], | |
| ["grpCryo","seasonalFrost","Seasonal frost halo",0,1,.01], | |
| ["grpCryo","linea","Tidal fractures (lineae)",0,1,.01], | |
| ["grpCryo","doubleRidge","Double ridges / cycloids",0,1,.01], | |
| ["grpCryo","tiger","Tiger stripes (geysers)",0,1,.01], | |
| ["grpCryo","sulci","Grooved terrain (sulci)",0,1,.01], | |
| ["grpCryo","chaos","Chaos terrain",0,1,.01], | |
| ["grpCryo","nitrogenGlacier","Nitrogen glacier (Sputnik)",0,1,.01], | |
| ["grpCryo","sublimation","Sublimation / bladed terrain",0,1,.01], | |
| ["grpCryo","equatorRidge","Equatorial ridge",0,1,.01], | |
| ["grpCryo","cryoDome","Cryovolcanic domes",0,1,.01], | |
| ["grpCryo","cantaloupe","Cantaloupe terrain (Triton)",0,1,.01], | |
| ["grpCryo","wispy","Wispy fracture terrain",0,1,.01], | |
| ["grpCryo","polygons","Contraction polygons",0,1,.01], | |
| ["grpCryo","pldSpiral","Polar layered spiral troughs",0,1,.01], | |
| ["grpCryo","glacialFlow","Glacial flow tongues (N₂)",0,1,.01], | |
| ["grpCryo","frozenSea","Frozen-over seas",0,1,.01], | |
| ["grpCryo","seaIce","Sea ice / leads",0,1,.01], | |
| ["grpCryo","spiders","Araneiform CO₂ spiders",0,1,.01], | |
| ["grpAlb","albedoProv","Albedo provinces (relief-free)",0,1,.01], | |
| ["grpAlb","dichotomy","Hemispheric crustal dichotomy",0,1,.01], | |
| ["grpAlb","hemiAlbedo","Hemispheric albedo (Iapetus)",0,1,.01], | |
| ["grpAlb","polarTholin","Polar tholin cap (Charon)",0,1,.01], | |
| ["grpAlb","slopeStreaks","Slope streaks / RSL",0,1,.01], | |
| ["grpAlb","windStreaks","Wind streaks (downwind tails)",0,1,.01], | |
| ["grpAlb","swirls","Magnetic swirls (Reiner Gamma)",0,1,.01], | |
| ["grpAlb","provinces","Sharp compositional provinces",0,1,.01], | |
| ["grpAlb","faculae","Bright salt deposits (faculae)",0,1,.01], | |
| ["grpAlb","latBands","Latitudinal albedo bands",0,1,.01], | |
| ["grpAlb","devilTracks","Dust-devil tracks",0,1,.01], | |
| ["grpAtmo","atmThickness","Atmosphere thickness (scale height)",0,1,.01], | |
| ["grpAtmo","rayleigh","Rayleigh scatter (blue limb + sunset)",0,1,.01], | |
| ["grpAtmo","haze","Aerosol haze veil (Mie)",0,1,.01], | |
| ["grpAtmo","hazeLayers","Detached haze layers (Titan/Pluto)",0,1,.01], | |
| ["grpAtmo","limbDarkening","Photometric limb darkening",0,1,.01], | |
| ["grpAtmo","clouds","Cloud cover",0,1,.01], | |
| ["grpAtmo","cloudOpacity","Cloud opacity",0,1,.01], | |
| ["grpAtmo","cloudShadows","Cloud shadows on surface",0,1,.01], | |
| ["grpAtmo","cyclones","Cyclonic storms (spiral weather)",0,1,.01], | |
| ["grpAtmo","multiDeck","Multi-deck clouds",0,1,.01], | |
| ["grpAtmo","lightning","Lightning (night side)",0,1,.01], | |
| ["grpAtmo","polarHood","Polar haze hood",0,1,.01], | |
| ["grpAtmo","dustStorm","Global dust storm",0,1,.01], | |
| ["grpAtmo","atmoGlow","Atmosphere limb glow",0,2,.01], | |
| ["grpAtmo","cityLights","Night-side city lights",0,1,.01], | |
| ["grpAtmo","oppSurge","Opposition surge",0,1,.01], | |
| ["grpAtmo","bands","Cloud bands (gas)",2,16,1], | |
| ["grpAtmo","bandWarp","Jet-stream turbulence",0,1,.01], | |
| ["grpAtmo","bandIrregular","Jet spacing irregularity",0,1,.01], | |
| ["grpAtmo","altColor","Altitude-coded color",0,1,.01], | |
| ["grpAtmo","bandContrast","Band contrast (Uranus↔Jupiter)",0,1,.01], | |
| ["grpAtmo","storms","Storm vortices",0,1,.01], | |
| ["grpAtmo","polarVortex","Polar cyclone clusters",0,1,.01], | |
| ["grpAtmo","hexagon","Polar hexagon (Saturn)",0,1,.01], | |
| ["grpRing","rings","Rings on/off",0,1,1], | |
| ["grpRing","ringInner","Ring inner radius",1.1,2,.01], | |
| ["grpRing","ringOuter","Ring outer radius",1.5,3,.01], | |
| ["grpRing","ringOpacity","Ring opacity",0,1,.01], | |
| ["grpRing","moons","Moons",0,3,1], | |
| ["grpRing","tail","Comet tail / coma",0,1,.01], | |
| ["grpRing","ringSpokes","Ring spokes",0,1,.01], | |
| ["grpRing","ringForward","Ring forward-scatter (backlit)",0,1,.01], | |
| ["grpAtmo","aurora","Auroral ovals (polar)",0,1,.01], | |
| ["grpFig","tidalLock","Tidal lock (substellar↔antistellar)",0,1,.01], | |
| ["grpEndo","lavaWorld","Magma ocean / lava self-emission",0,1,.01], | |
| ["grpCryo","frozen","Global glaciation (snowball)",0,1,.01], | |
| ["grpColor","oxidation","Oxidation / rust",0,1,.01], | |
| ["grpColor","tholin","Tholin haze (organics)",0,1,.01], | |
| ["grpColor","sulfur","Sulfur volcanism",0,1,.01] | |
| ]; | |
| const RENDER_ONLY=new Set(["oblate","axialTilt","sunAz","sunEl","ambient","atmoGlow","oppSurge","dustStorm","rings","ringInner","ringOuter","ringOpacity","moons","cloudOpacity","tidalLock","lavaWorld","frozen","aurora","tail","atmThickness","rayleigh","haze","hazeLayers","limbDarkening","cloudShadows","castShadow","starTemp","earthshine","lommel","lightning","ringSpokes","polarHood","ringForward"]); | |
| const rowRefs={}; | |
| function makeSlider(key,label,mn,mx,st){ | |
| const row=document.createElement("div");row.className="row"; | |
| row.innerHTML=`<label>${label}</label><input type="range" min="${mn}" max="${mx}" step="${st}" value="${S[key]}"><span class="val"></span>`; | |
| const inp=row.querySelector("input"),val=row.querySelector(".val"); | |
| const show=()=>val.textContent=(+S[key]).toFixed(st<1?2:0); | |
| inp.addEventListener("input",()=>{S[key]=+inp.value;show();RENDER_ONLY.has(key)?null:scheduleBuild();}); | |
| show();row._sync=()=>{inp.value=S[key];show();};return row; | |
| } | |
| SLIDERS.forEach(([grp,key,label,mn,mx,st])=>{const row=makeSlider(key,label,mn,mx,st);rowRefs[key]=row;document.getElementById(grp).appendChild(row);}); | |
| (function(){const row=document.createElement("div");row.className="row"; | |
| row.innerHTML=`<label>Palette</label><select id="palSel" style="flex:1.3"></select>`; | |
| document.getElementById("grpColor").prepend(row);const sel=row.querySelector("#palSel"); | |
| Object.keys(PALETTES).forEach(k=>{const o=document.createElement("option");o.value=k;o.textContent=k;sel.appendChild(o);}); | |
| sel.value=S.palette;sel.addEventListener("change",()=>{S.palette=sel.value;scheduleBuild();});rowRefs._pal=sel;})(); | |
| const presetSel=document.getElementById("preset"); | |
| Object.keys(PRESETS).forEach(name=>{const o=document.createElement("option");o.value=name;o.textContent=name;presetSel.appendChild(o);}); | |
| presetSel.value=S.preset; | |
| presetSel.addEventListener("change",()=>applyPreset(presetSel.value)); | |
| function applyPreset(name){const p=PRESETS[name];if(!p)return;Object.assign(S,D);Object.assign(S,p); | |
| S.preset=name;S.seed=+document.getElementById("seed").value;syncUI();build();} | |
| function syncUI(){Object.values(rowRefs).forEach(r=>{if(r&&r._sync)r._sync();});if(rowRefs._pal)rowRefs._pal.value=S.palette;presetSel.value=S.preset;} | |
| let buildTimer=null;function scheduleBuild(){clearTimeout(buildTimer);buildTimer=setTimeout(build,150);} | |
| document.getElementById("seed").addEventListener("change",e=>{S.seed=+e.target.value;build();}); | |
| document.getElementById("reseed").addEventListener("click",()=>{S.seed=(Math.random()*99999)|0;document.getElementById("seed").value=S.seed;build();}); | |
| document.getElementById("build").addEventListener("click",build); | |
| document.getElementById("surprise").addEventListener("click",()=>{const names=Object.keys(PRESETS); | |
| applyPreset(names[(Math.random()*names.length)|0]);S.seed=(Math.random()*99999)|0;document.getElementById("seed").value=S.seed; | |
| ["tectonics","craters","volcano","erosion","dunes","oxidation","craterSize"].forEach(k=>{if(S[k]!==undefined)S[k]=clamp(S[k]+(Math.random()-0.5)*0.4,0,1);}); | |
| syncUI();build();}); | |
| document.getElementById("spinToggle").addEventListener("click",e=>{autoSpin=!autoSpin;e.target.textContent=autoSpin?"⏸ spin":"▶ spin";}); | |
| document.getElementById("save").addEventListener("click",()=>{const a=document.createElement("a");a.download="planetlab_"+S.seed+".png";a.href=view.toDataURL("image/png");a.click();}); | |
| document.querySelectorAll("[data-sec]>.hd").forEach(hd=>hd.addEventListener("click",()=>hd.parentElement.classList.toggle("open"))); | |
| function hud(extra){document.getElementById("hud").innerHTML= | |
| `<b>${S.preset.replace(/^[^ ]+ /,"")}</b><br>mode: ${S.mode} · palette: ${S.palette}<br>seed ${S.seed} · ${extra||""}`;} | |
| function drawPipe(){const a=[];const add=(c,k)=>{if(S[k]>0)a.push(c);}; | |
| if(S.irregular>0)a.push("irregular-figure");if(S.oblate>0.01)a.push("oblate"); | |
| if(S.mode==="gas"){a.push("zonal-jets");if(S.storms>0)a.push("storms");if(S.hexagon>0)a.push("hexagon");if(S.polarVortex>0)a.push("polar-cyclones");} | |
| else{add("tectonics","tectonics");add("orogeny","mountains");add("scarps","scarps");add("volcanism","volcano"); | |
| add("maria","floods");add("impacts","craters");add("relaxation","relaxation");add("multiring","multiring"); | |
| add("erosion","erosion");add("strata","stratify");add("aeolian","dunes");add("weathering","weather"); | |
| add("ocean","sea");add("ice","iceCap");add("lineae","linea");add("sulci","sulci");add("N₂-glacier","nitrogenGlacier"); | |
| add("sublimation","sublimation");add("dichotomy","dichotomy");add("albedo-prov","albedoProv");} | |
| if(S.clouds>0)a.push("clouds");if(S.rings>0)a.push("rings");if(S.moons>0)a.push("moons"); | |
| document.getElementById("pipe").innerHTML="pipeline → "+a.map(x=>`<span class="on">${x}</span>`).join(" → ");} | |
| /* codex */ | |
| const CODEX=[ | |
| ["0 · Figure & illumination (whole-disk geometry)",[ | |
| ["Rotational oblateness ●","Jupiter, Saturn","Fast spin flattens the figure into an ellipsoid — visible in the silhouette."], | |
| ["Irregular / triaxial figure ●","Vesta, Phobos","Bodies below ~400 km don't relax to spheres; the lumpy outline is their identity."], | |
| ["Contact-binary lobes ●","67P, Arrokoth","Two bodies merged at low speed → bilobed peanut shape."], | |
| ["Phase / terminator ●","every spacecraft image","Sun direction sets the day/night line; orbital photos are rarely full disks."], | |
| ["Opposition surge ●","full Moon","Regolith backscatters light straight back near zero phase angle."], | |
| ["Tidal locking ●","Io, Moon near side, hot Jupiters","Synchronous rotation fixes a substellar point → permanent day/night asymmetry."], | |
| ["Cast shadows ●","low-sun Moon/Vesta","Topography throws long shadows toward the terminator."], | |
| ["Lommel-Seeliger law ●","full Moon","Regolith scatters to a flat, even disk — not limb-darkened Lambert."], | |
| ["Star color temperature ●","red-dwarf planets","The illuminating star's color tints the lit hemisphere."], | |
| ["Earthshine ●","new-Moon crescent","A companion's reflected light faintly lifts the night side."]]], | |
| ["A · Endogenic (internal heat & tectonics)",[ | |
| ["Tectonism ●","Mars rifts, Earth ranges","Crust stretches/folds → grabens, fold belts."], | |
| ["Lobate thrust scarps ●","Mercury","Global contraction thrusts crust over itself in one-sided cliffs."], | |
| ["Wrinkle ridges ●","lunar maria","Compression of lava plains into branching ridges."], | |
| ["Shield volcanism ●","Olympus Mons","Hotspot basalt stacks into giant edifices + collapse calderas."], | |
| ["Flood basalts / maria ●","lunar seas","Effusive lava drowns lowlands into dark plains."], | |
| ["Magma ocean / lava self-emission ●","Io, early Earth, lava worlds","Molten rock glows on its own — visible even on the night side."], | |
| ["Rift valleys / grabens ●","Valles Marineris, E. Africa","Crust pulls apart into down-dropped linear troughs."], | |
| ["Coronae ●","Venus","Mantle upwelling makes ring-fracture / annular structures."], | |
| ["Tesserae ●","Venus highlands","Intensely deformed crosshatched-ridge terrain."], | |
| ["Mantle-plume swell ●","Tharsis (Mars)","Broad regional uplift doming a whole province."], | |
| ["Volcanic plains / plateaus ●","Deccan, Lakshmi Planum","Constructional positive-relief flood-basalt plateaus."]]], | |
| ["B · Exogenic (impacts, water, wind, ice)",[ | |
| ["Impact cratering ●","the Moon","Bowls, rims, central peaks, ejecta rays; density = age."], | |
| ["Viscous relaxation ●","Callisto palimpsests","Warm ice flattens craters into ghost patches."], | |
| ["Multi-ring basins ●","Orientale, Valhalla","Giant impacts make concentric ring systems."], | |
| ["Secondary chains ●","all big impacts","Ejecta blocks gouge radial crater chains."], | |
| ["Hydraulic erosion ●","Mars outflow, Earth","Water carves valleys, canyons, channels."], | |
| ["Aeolian ●","Titan dunes","Wind builds dunes, ripples, streaks, mantling."], | |
| ["Sedimentary strata ●","Mars PLD","Layered deposition exposed as banding."], | |
| ["Space weathering ●","airless bodies","Solar wind darkens/reddens mature surfaces."], | |
| ["Hydraulic rivers ●","Mars, Earth","Flow-routing carves dendritic valleys, deltas & lakebeds."], | |
| ["Mass wasting ●","Valles Marineris walls","Oversteepened slopes fail into landslides & talus."], | |
| ["Wind erosion / yardangs ●","Medusae Fossae","Wind flutes soft deposits into parallel ridges."], | |
| ["Glacial erosion ●","Yosemite, Mars","Ice carves U-valleys & flattens valley floors."], | |
| ["Coastlines / benches ●","Earth, Mars shoreline","Waves cut a bench + sea-cliff at the waterline."], | |
| ["Inverted relief ●","Mars channels","Indurated lows stand proud as terrain deflates."], | |
| ["Crater degradation ●","lunar highlands","Old craters soften and infill with age."]]], | |
| ["C · Cryo / ice / fluids",[ | |
| ["Tidal lineae ●","Europa","Tidal stress cracks the ice shell."], | |
| ["Double ridges / cycloids ●","Europa","Raised twin ridges along scalloped arcs."], | |
| ["Tiger stripes ●","Enceladus","Warm fractures vent geyser plumes."], | |
| ["Grooved terrain (sulci) ●","Ganymede","Swaths of densely packed parallel grooves."], | |
| ["Chaos terrain ●","Europa","Diapirism breaks crust into jumbled blocks."], | |
| ["Nitrogen glacier ●","Pluto Sputnik P.","Bright N₂ ice fills basins, convects in polygons."], | |
| ["Sublimation / blades ●","Pluto, comets","Ice sublimes into pits and bladed ridges."], | |
| ["Hydrocarbon seas ●","Titan","Dark methane/ethane lakes, polar."], | |
| ["Equatorial ridge ●","Iapetus","A sharp belt of mountains along the equator."], | |
| ["Global glaciation (snowball) ●","Snowball Earth, Mars ice ages","Runaway ice-albedo freezes the entire surface white."], | |
| ["Cryovolcanic domes ●","Ahuna Mons (Ceres)","Viscous icy lava builds domes & lobate flows."], | |
| ["Cantaloupe terrain ●","Triton","Dimpled cellular diapir terrain."], | |
| ["Wispy fracture terrain ●","Dione, Rhea","Bright braided networks of fracture cliffs."], | |
| ["Contraction polygons ●","Triton, Mars permafrost","Thermal contraction cracks ice into polygons."], | |
| ["Polar layered spirals ●","Mars PLD","Spiral troughs cut the layered polar ice deposits."], | |
| ["Glacial flow tongues ●","Pluto Sputnik P.","N₂ ice overtops basins and creeps out in lobate tongues."], | |
| ["Frozen-over seas ●","Titan winter lakes","A cold sea freezes a flat bright ice lid over the basin."], | |
| ["Sea ice / leads ●","Earth pack ice","Floating ice cracks into floes split by dark open-water leads."], | |
| ["Araneiform spiders ●","Mars S. polar","Sublimating CO₂ gas carves radial dendritic dark fans."]]], | |
| ["D · Albedo provinces (decoupled from relief)",[ | |
| ["Albedo provinces ●","Moon, Mars dust","Composition/dust maps independent of topography."], | |
| ["Crustal dichotomy ●","Mars","Degree-1 hemispheric asymmetry + crater-age gradient."], | |
| ["Hemispheric albedo ●","Iapetus","Dust infall darkens one whole hemisphere."], | |
| ["Polar tholin cap ●","Charon","Cold-trapped gas reddens a pole."], | |
| ["Slope streaks / RSL ●","Mars","Downslope dark lineae, pure albedo."], | |
| ["Wind streaks ●","Mars","Bright/dark tails deposited downwind of craters."], | |
| ["Magnetic swirls ●","Moon (Reiner Gamma)","Sinuous bright patterns unrelated to topography."], | |
| ["Compositional provinces ●","Moon mare/highland","Sharp-edged distinct albedo/terrain units."], | |
| ["Faculae / salt deposits ●","Ceres (Occator)","Compact, very-bright evaporite spots."], | |
| ["Latitudinal albedo bands ●","Mars, Triton","Broad bright/dark zonal belts by latitude."], | |
| ["Dust-devil tracks ●","Mars","Criss-crossing dark trails where dust is scoured off."]]], | |
| ["E · Atmosphere, clouds & companions",[ | |
| ["Atmosphere thickness ●","Venus thick ↔ Mars thin","Scale height sets limb extent, twilight width & surface veiling."], | |
| ["Rayleigh scattering ●","Earth blue limb","Molecular scatter → blue day-limb + reddened sunset ring at the terminator."], | |
| ["Aerosol haze veil (Mie) ●","Venus, Titan","Smog desaturates the disk and can fully hide the surface."], | |
| ["Detached haze layers ●","Titan, Pluto","Stacked aerosol layers form concentric bright arcs above the limb."], | |
| ["Limb darkening ●","gas giants","Photometric darkening of the disk toward the edge."], | |
| ["Cloud shadows ●","Earth, Jupiter","Cloud decks cast shadows on the surface/deck below."], | |
| ["Zonal jets / bands ●","Jupiter, Saturn","Convection + rotation → alternating belts/zones."], | |
| ["Altitude-coded color ●","Jupiter","Cloud-deck height sets brown↔white."], | |
| ["Storm vortices ●","Great Red Spot","Long-lived anticyclones between jets."], | |
| ["Polar cyclone clusters ●","Jupiter (Juno)","Rings of cyclones around the poles."], | |
| ["Polar hexagon ●","Saturn","Standing 6-lobed polar jet."], | |
| ["Cloud layer over surface ●","Earth, Venus, Titan","Semi-opaque clouds (Venus hides the surface)."], | |
| ["Global dust storm ●","Mars","Haze washes out the whole disk."], | |
| ["Axial tilt / obliquity ●","Uranus (98°)","Tilts the spin axis & ring plane — seasons; Uranus lies on its side."], | |
| ["Rings + divisions + shadows ●","Saturn","Ice annulus with Cassini/Encke gaps; mutual ring/planet shadowing."], | |
| ["Moons + transit shadows ●","Jovian system","Satellites cast shadow dots on the primary."], | |
| ["Auroral ovals ●","Earth, gas giants","Magnetospheric emission ringing the poles on the night side."], | |
| ["Comet coma + tail ●","comets, evaporating planets","Sublimating volatiles form a coma and an anti-sunward tail."], | |
| ["City lights (night side) ●","Earth","Biosignature emissive on the dark hemisphere."], | |
| ["Cyclonic weather ●","Earth hurricanes, Venus Y","Spiral storm systems & fronts in the cloud layer."], | |
| ["Multi-deck clouds ●","Jupiter, Earth","Stacked cloud decks; gaps reveal the layer below."], | |
| ["Lightning ●","Jupiter, Earth night side","Transient flashes in storm clouds on the dark side."], | |
| ["Ring spokes ●","Saturn B-ring","Radial markings rotating across the rings."], | |
| ["Ring forward-scatter ●","Saturn backlit","Rings flare bright when backlit at high phase angle."], | |
| ["Eclipse penumbra ●","moon transit shadow","Soft umbra→penumbra gradient at a cast shadow's edge."], | |
| ["Dust + ion comet tails ●","comets","A curved warm dust tail offset from the straight blue ion tail."], | |
| ["Polar haze hood ●","Mars, Titan","Aerosol concentrates into a darker polar cap of haze."]]] | |
| ]; | |
| (function(){let html="";CODEX.forEach(([cat,rows])=>{html+=`<div class="cat">${cat}</div><table>`; | |
| rows.forEach(([a,b,c])=>html+=`<tr><td>${a}</td><td>${b}</td><td>${c}</td></tr>`);html+="</table>";}); | |
| html+=`<div class="cx" style="margin-top:22px">● = adjustable layer in this build. A world's whole-disk | |
| appearance = figure × illumination × (which processes ran, how hard, in what order) × chemistry × companions.</div>`; | |
| document.getElementById("codexBody").innerHTML=html;})(); | |
| document.getElementById("codexBtn").addEventListener("click",()=>document.getElementById("codex").classList.add("show")); | |
| document.getElementById("codexClose").addEventListener("click",()=>document.getElementById("codex").classList.remove("show")); | |
| /* GO — supports planetlab.html?p=<name substring> to deep-link a preset and hold still for capture */ | |
| (function(){const q=new URLSearchParams(location.search).get("p")||(location.hash?decodeURIComponent(location.hash.slice(1)):""); | |
| let start=S.preset; | |
| if(q){const hit=Object.keys(PRESETS).find(n=>n.toLowerCase().includes(q.toLowerCase()));if(hit)start=hit;} | |
| applyPreset(start); | |
| if(q){autoSpin=false;const t=document.getElementById("spinToggle");if(t)t.textContent="▶ spin";} | |
| })(); | |
| window.addEventListener("hashchange",()=>{const q=location.hash?decodeURIComponent(location.hash.slice(1)):"";if(!q)return; | |
| const hit=Object.keys(PRESETS).find(n=>n.toLowerCase().includes(q.toLowerCase())); | |
| if(hit){applyPreset(hit);autoSpin=false;const t=document.getElementById("spinToggle");if(t)t.textContent="▶ spin";}}); | |
| loop(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment