chore: add reference docs, scripts, tests, demo, prototypes
- Reference docs: known issues, user journey, location redesign, mission merge plan - Scripts: fill_location_state, merge_missions, migrate_location, migrate_state - Tests: e2e test suite, asset entries unit tests - Demo: location board, hifi prototypes, test screenshots - Data backup: asset log entries and state nodes - Update .gitignore Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KSP管理后台 — Location Board</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0B0B10;--surface:#131318;--border:rgba(248,250,252,.08);--text:#F8FAFC;--muted:#94A3B8;
|
||||
--accent:#3B82F6;--accent-glow:rgba(59,130,246,.35);--warn:#eab308;--danger:#EF4444;
|
||||
--radius:8px;--transition:150ms ease;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{
|
||||
background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,'Segoe UI',sans-serif;
|
||||
height:100vh;overflow:hidden;display:flex;flex-direction:column;
|
||||
}
|
||||
/* Top bar */
|
||||
#top-bar{
|
||||
display:flex;align-items:center;gap:12px;padding:8px 16px;
|
||||
background:var(--surface);border-bottom:1px solid var(--border);z-index:20;
|
||||
font-size:.8rem;
|
||||
}
|
||||
#top-bar .brand{font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-weight:600;font-size:.84rem;color:var(--text);letter-spacing:-.02em}
|
||||
#top-bar .brand span{color:var(--accent)}
|
||||
#scope-label{font-size:.78rem;color:var(--accent);font-weight:500}
|
||||
.spacer{flex:1}
|
||||
#time-controls{display:flex;align-items:center;gap:4px}
|
||||
#time-controls button,.style-btn{
|
||||
background:rgba(255,255,255,.05);border:1px solid transparent;color:var(--muted);
|
||||
padding:4px 9px;border-radius:4px;cursor:pointer;font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.72rem;
|
||||
transition:all var(--transition);
|
||||
}
|
||||
#time-controls button:hover{background:rgba(255,255,255,.1);color:var(--text);border-color:var(--border)}
|
||||
.style-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||||
#sim-time-input{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.78rem;padding:4px 8px;border-radius:4px;
|
||||
background:rgba(255,255,255,.04);color:var(--text);border:1px solid var(--border);width:152px;text-align:center;
|
||||
}
|
||||
#sim-time-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}
|
||||
#time-slider{
|
||||
flex:1;min-width:100px;max-width:240px;-webkit-appearance:none;height:3px;
|
||||
background:var(--border);border-radius:2px;outline:none;cursor:pointer;
|
||||
}
|
||||
#time-slider::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:var(--accent);cursor:pointer;box-shadow:0 0 10px var(--accent-glow)}
|
||||
/* Main layout */
|
||||
#main{display:flex;flex:1;overflow:hidden}
|
||||
/* Map */
|
||||
#map-container{
|
||||
flex:1;position:relative;display:flex;align-items:center;justify-content:center;
|
||||
background:var(--bg);cursor:grab;overflow:hidden;
|
||||
}
|
||||
canvas{display:block}
|
||||
/* Map overlays */
|
||||
#back-btn{
|
||||
position:absolute;top:12px;left:12px;z-index:5;display:none;
|
||||
background:var(--surface);border:1px solid var(--border);color:var(--text);
|
||||
padding:5px 14px;border-radius:var(--radius);cursor:pointer;
|
||||
font-family:'Fira Sans',sans-serif;font-size:.78rem;
|
||||
transition:all var(--transition);
|
||||
}
|
||||
#back-btn.visible{display:block}
|
||||
#back-btn:hover{background:rgba(255,255,255,.08);border-color:rgba(248,250,252,.15)}
|
||||
#scope-indicator{
|
||||
position:absolute;top:12px;right:12px;z-index:5;
|
||||
background:rgba(19,19,24,.85);border:1px solid var(--border);
|
||||
color:var(--muted);padding:4px 12px;border-radius:4px;font-size:.72rem;
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;
|
||||
}
|
||||
#style-toggle{position:absolute;bottom:12px;left:12px;z-index:5;display:flex;gap:4px}
|
||||
#instructions{
|
||||
position:absolute;bottom:12px;right:12px;z-index:5;
|
||||
color:rgba(148,163,184,.4);font-size:.66rem;font-family:'Fira Sans',sans-serif;
|
||||
pointer-events:none;
|
||||
}
|
||||
/* Info panel */
|
||||
#info-panel{
|
||||
width:320px;background:var(--surface);border-left:1px solid var(--border);
|
||||
padding:20px 18px;overflow-y:auto;display:flex;flex-direction:column;gap:18px;
|
||||
}
|
||||
#info-panel h2{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.88rem;font-weight:600;
|
||||
padding-bottom:6px;border-bottom:1px solid var(--border);letter-spacing:-.02em;
|
||||
}
|
||||
.info-section{}
|
||||
.info-section h3{
|
||||
font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.66rem;text-transform:uppercase;
|
||||
color:var(--muted);letter-spacing:.06em;margin-bottom:4px;font-weight:500;
|
||||
}
|
||||
.metric-value{font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:1.5rem;font-weight:600;color:var(--accent)}
|
||||
.asset-row{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
padding:6px 0;border-bottom:1px solid rgba(248,250,252,.04);font-size:.8rem;
|
||||
}
|
||||
.asset-row .name{color:var(--accent);cursor:pointer;transition:color var(--transition)}
|
||||
.asset-row .name:hover{color:#60a5fa;text-decoration:underline}
|
||||
.asset-row .loc{color:var(--muted);font-size:.72rem;font-family:'SF Mono','Cascadia Code','Consolas',monospace}
|
||||
.event-row{
|
||||
padding:4px 0;font-size:.78rem;border-bottom:1px solid rgba(248,250,252,.04);
|
||||
}
|
||||
.event-row .date{color:var(--warn);font-family:'SF Mono','Cascadia Code','Consolas',monospace;font-size:.72rem}
|
||||
/* Scrollbar */
|
||||
#info-panel::-webkit-scrollbar{width:4px}
|
||||
#info-panel::-webkit-scrollbar-track{background:transparent}
|
||||
#info-panel::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="top-bar">
|
||||
<div class="brand"><span>KSP</span>管理后台</div>
|
||||
<span style="color:var(--muted);font-size:.7rem">/</span>
|
||||
<span id="scope-label">Solar System</span>
|
||||
<span class="spacer"></span>
|
||||
<div id="time-controls">
|
||||
<button onclick="shiftDays(-365)" title="-1 year">◀◀</button>
|
||||
<button onclick="shiftDays(-30)" title="-30 days">◀</button>
|
||||
<input type="text" id="sim-time-input" value="2060-08-11 10:00" title="YYYY-MM-DD HH:MM">
|
||||
<button onclick="shiftDays(30)" title="+30 days">▶</button>
|
||||
<button onclick="shiftDays(365)" title="+1 year">▶▶</button>
|
||||
<button onclick="resetTime()">Today</button>
|
||||
</div>
|
||||
<input type="range" id="time-slider" min="0" max="1000" value="0">
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div id="map-container">
|
||||
<button id="back-btn" onclick="goBack()">← Back</button>
|
||||
<span id="scope-indicator">Solar System</span>
|
||||
<div id="style-toggle">
|
||||
<button class="style-btn" id="btn-flat" onclick="setStyle('flat')">Flat</button>
|
||||
<button class="style-btn active" id="btn-realistic" onclick="setStyle('realistic')">Realistic</button>
|
||||
</div>
|
||||
<span id="instructions">click planet to focus · drag to pan · scroll to zoom</span>
|
||||
<canvas id="map-canvas"></canvas>
|
||||
</div>
|
||||
<div id="info-panel">
|
||||
<h2 id="panel-title">Solar System</h2>
|
||||
<div class="info-section"><h3>Scope</h3><div style="font-size:.92rem;font-weight:500" id="panel-scope">—</div></div>
|
||||
<div class="info-section"><h3>Assets in Scope</h3><div id="panel-assets"></div></div>
|
||||
<div class="info-section"><h3>Ongoing Missions</h3><div class="metric-value" id="panel-mission-count">—</div></div>
|
||||
<div class="info-section"><h3>Upcoming Events</h3><div id="panel-events"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ═══════════════════════════════════
|
||||
// KEPLERIAN ORBITAL ELEMENTS (J2000)
|
||||
// ═══════════════════════════════════
|
||||
const J2000_JD=2451545.0,DAY=86400000;
|
||||
const ORB={
|
||||
mercury:{a:.387098,e:.205630,i:7.005,O:48.331,p:77.456,L:252.251,P:87.969},
|
||||
venus:{a:.723332,e:.006772,i:3.395,O:76.680,p:131.563,L:181.980,P:224.701},
|
||||
earth:{a:1.000003,e:.016709,i:.000,O:.000,p:102.938,L:100.464,P:365.256},
|
||||
mars:{a:1.523679,e:.093401,i:1.850,O:49.558,p:336.060,L:355.453,P:686.980},
|
||||
ceres:{a:2.767500,e:.075823,i:10.593,O:80.306,p:72.716,L:73.598,P:1680},
|
||||
vesta:{a:2.361000,e:.088740,i:7.141,O:103.852,p:150.047,L:18.900,P:1325},
|
||||
jupiter:{a:5.203363,e:.048393,i:1.305,O:100.556,p:14.754,L:34.404,P:4332.589},
|
||||
saturn:{a:9.537070,e:.054151,i:2.485,O:113.715,p:92.432,L:49.944,P:10759.22},
|
||||
uranus:{a:19.19126,e:.047168,i:.770,O:74.230,p:170.964,L:313.232,P:30685.4},
|
||||
neptune:{a:30.06904,e:.008606,i:1.770,O:131.723,p:44.971,L:304.880,P:60189},
|
||||
pluto:{a:39.48212,e:.248828,i:17.140,O:110.304,p:224.069,L:238.929,P:90560}
|
||||
};
|
||||
function jd(d){return d.getTime()/DAY+2440587.5}
|
||||
function kepler(M,e,eps=1e-8){let E=M+e*Math.sin(M)*(1+e*Math.cos(M));for(let i=0;i<20;i++){let d=(M-E+e*Math.sin(E))/(1-e*Math.cos(E));E+=d;if(Math.abs(d)<eps)break}return E}
|
||||
function helio(key,jv){
|
||||
const o=ORB[key];if(!o)return{x:0,y:0,r:0};const D=Math.PI/180,n=2*Math.PI/o.P,ds=jv-J2000_JD;
|
||||
let M=(o.L-o.p)*D+n*ds;M=((M%(2*Math.PI))+2*Math.PI)%(2*Math.PI);
|
||||
const E=kepler(M,o.e),s=Math.sin(E),c=Math.cos(E),r=o.a*(1-o.e*c);
|
||||
const sv=Math.sqrt(1-o.e*o.e)*s/(1-o.e*c),cv=(c-o.e)/(1-o.e*c);
|
||||
const w=(o.p-o.O)*D,O=o.O*D,I=o.i*D,cO=Math.cos(O),sO=Math.sin(O),cI=Math.cos(I),sI=Math.sin(I);
|
||||
const xo=r*cv,yo=r*sv,cw=Math.cos(w),sw=Math.sin(w);
|
||||
const xe=xo*(cO*cw-sO*sw*cI)-yo*(cO*sw+sO*cw*cI);
|
||||
const ye=xo*(sO*cw+cO*sw*cI)+yo*(cO*cw*cI-sO*sw);
|
||||
const dist=Math.sqrt(xe*xe+ye*ye),ddisp=Math.pow(dist,.4)*100,scl=dist>.001?ddisp/dist:0;
|
||||
return{x:xe*scl,y:ye*scl,r:ddisp}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// BODIES
|
||||
// ═══════════════════════════════════
|
||||
const BD={
|
||||
sun:{n:'Sun',t:'star',r:20,fr:52,c:'#FDB813',c2:'#ff6b00',moons:null},
|
||||
mercury:{n:'Mercury',t:'planet',r:5.5,fr:38,c:'#b5b5b5',c2:'#8a8a8a',moons:null},
|
||||
venus:{n:'Venus',t:'planet',r:8,fr:44,c:'#e6c229',c2:'#c4952a',moons:null},
|
||||
earth:{n:'Earth',t:'planet',r:9,fr:46,c:'#4da6ff',c2:'#1a56db',
|
||||
moons:{moon:{n:'Moon',r:2.8,c:'#ccc',c2:'#999',orb:40,P:27.3}}},
|
||||
mars:{n:'Mars',t:'planet',r:7,fr:40,c:'#c1440e',c2:'#7a1f0a',
|
||||
moons:{phobos:{n:'Phobos',r:1.6,c:'#998',c2:'#665',orb:26,P:.32},
|
||||
deimos:{n:'Deimos',r:1.4,c:'#887',c2:'#554',orb:35,P:1.26}}},
|
||||
ceres:{n:'Ceres',t:'dwarf',r:4,fr:32,c:'#8b8b8b',c2:'#6b6b6b',moons:null},
|
||||
vesta:{n:'Vesta',t:'ast',r:3,fr:28,c:'#a0a0a0',c2:'#808080',moons:null},
|
||||
jupiter:{n:'Jupiter',t:'planet',r:16,fr:60,c:'#c9b07c',c2:'#8b6914',
|
||||
moons:{io:{n:'Io',r:2.8,c:'#e8d44d',c2:'#b8a420',orb:50,P:1.77},
|
||||
europa:{n:'Europa',r:2.5,c:'#d4c9a8',c2:'#a09060',orb:62,P:3.55},
|
||||
ganymede:{n:'Ganymede',r:3.2,c:'#aaa',c2:'#777',orb:76,P:7.15},
|
||||
callisto:{n:'Callisto',r:2.8,c:'#666',c2:'#444',orb:90,P:16.7}}},
|
||||
saturn:{n:'Saturn',t:'planet',r:13.5,fr:54,c:'#e0c97f',c2:'#b8952a',
|
||||
moons:{mimas:{n:'Mimas',r:1.8,c:'#bbb',c2:'#999',orb:44,P:.94},
|
||||
enceladus:{n:'Enceladus',r:2,c:'#dde',c2:'#bbc',orb:52,P:1.37},
|
||||
tethys:{n:'Tethys',r:2.2,c:'#ddd',c2:'#bbb',orb:60,P:1.89},
|
||||
dione:{n:'Dione',r:2.3,c:'#ccc',c2:'#aaa',orb:68,P:2.74},
|
||||
rhea:{n:'Rhea',r:2.5,c:'#ddd',c2:'#bbb',orb:78,P:4.52},
|
||||
titan:{n:'Titan',r:3.5,c:'#d4a85c',c2:'#a07830',orb:90,P:15.95},
|
||||
iapetus:{n:'Iapetus',r:2.7,c:'#999',c2:'#666',orb:106,P:79.3}}},
|
||||
uranus:{n:'Uranus',t:'planet',r:11,fr:46,c:'#7ec8e3',c2:'#4a90b0',
|
||||
moons:{miranda:{n:'Miranda',r:1.6,c:'#ccc',c2:'#aaa',orb:36,P:1.41},
|
||||
ariel:{n:'Ariel',r:2.2,c:'#ddd',c2:'#bbb',orb:46,P:2.52},
|
||||
umbriel:{n:'Umbriel',r:2.1,c:'#999',c2:'#777',orb:54,P:4.14},
|
||||
titania:{n:'Titania',r:2.8,c:'#bbb',c2:'#999',orb:64,P:8.71},
|
||||
oberon:{n:'Oberon',r:2.5,c:'#aaa',c2:'#888',orb:74,P:13.46}}},
|
||||
neptune:{n:'Neptune',t:'planet',r:10,fr:44,c:'#3f54ba',c2:'#1a2a80',
|
||||
moons:{triton:{n:'Triton',r:2.8,c:'#b8d4e3',c2:'#80a0b0',orb:44,P:5.88}}},
|
||||
pluto:{n:'Pluto',t:'dwarf',r:3,fr:28,c:'#d4c9a8',c2:'#a09070',
|
||||
moons:{charon:{n:'Charon',r:2.2,c:'#bbb',c2:'#999',orb:28,P:6.39}}}
|
||||
};
|
||||
// Flatten moons into BD for direct lookup
|
||||
for(const bk of Object.keys(BD)){const b=BD[bk];if(b.moons)for(const[mk,mb]of Object.entries(b.moons))BD[mk]=mb}
|
||||
const SO=['mercury','venus','earth','mars','ceres','vesta','jupiter','saturn','uranus','neptune','pluto'];
|
||||
const MOCK_ASSETS=[
|
||||
{id:'st01',n:'ST-01 万星源号',loc:'Transfer',sc:['solar_system']},
|
||||
{id:'st02',n:'ST-02 克里斯滕号',loc:'Jupiter System',sc:['solar_system','jupiter']},
|
||||
{id:'st03',n:'ST-03 Muelsyse',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'xh01',n:'XH-01 羲和号',loc:'Jupiter System',sc:['solar_system','jupiter']},
|
||||
{id:'xh02',n:'XH-02 太白号',loc:'Mars Orbit',sc:['solar_system','mars']},
|
||||
{id:'xh03',n:'XH-03 常曦号',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'jw01',n:'JW-01 金乌号',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'sp',n:'星港空间站',loc:'LEO',sc:['solar_system','earth']},
|
||||
{id:'m1',n:'火星一号基地',loc:'Mars Surface',sc:['solar_system','mars']},
|
||||
{id:'eo',n:'欧罗巴前哨站',loc:'Europa Surface',sc:['solar_system','jupiter']},
|
||||
];
|
||||
const MOCK_EVENTS=[
|
||||
{d:'2060-06-10',t:'ST-03 维护结束'},{d:'2060-06-15',t:'ST-03 深空测试开始'},
|
||||
{d:'2060-06-28',t:'ST-02 返航抵达地球'},{d:'2060-07-25',t:'ST-01 抵达海王星系统'},
|
||||
{d:'2060-09-01',t:'ST-02 进入升级'},{d:'2060-10-25',t:'ST-03 服役前维护结束'},
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// STARFIELD & REAL TEXTURES
|
||||
// ═══════════════════════════════════
|
||||
let stars=null;
|
||||
function mkStars(w,h){
|
||||
const c=document.createElement('canvas');c.width=w;c.height=h;const x=c.getContext('2d');
|
||||
x.fillStyle='#0B0B10';x.fillRect(0,0,w,h);
|
||||
for(let i=0;i<800;i++){const px=Math.random()*w,py=Math.random()*h,s=Math.random()*1.6+.15;x.fillStyle=`rgba(255,255,255,${.3+Math.random()*.7})`;x.beginPath();x.arc(px,py,s,0,Math.PI*2);x.fill()}
|
||||
return c
|
||||
}
|
||||
|
||||
// Real texture images loaded from textures/ folder
|
||||
const TEX_SIZE=512;
|
||||
const TEX_MAP={
|
||||
sun:'sun.jpg',mercury:'mercury.jpg',venus:'venus.jpg',earth:'earth.jpg',
|
||||
mars:'mars.jpg',jupiter:'jupiter.jpg',saturn:'saturn.jpg',uranus:'uranus.jpg',
|
||||
neptune:'neptune.jpg',pluto:'pluto.jpg',moon:'moon.jpg',io:'io.jpg'
|
||||
};
|
||||
const texImgs={};
|
||||
let texLoaded=0;
|
||||
const texTotal=Object.keys(TEX_MAP).length;
|
||||
|
||||
function loadTextures(){
|
||||
for(const[key,file]of Object.entries(TEX_MAP)){
|
||||
const img=new Image();
|
||||
img.onload=()=>{texImgs[key]=img;texLoaded++;if(texLoaded>=texTotal)render()};
|
||||
img.src='textures/'+file;
|
||||
}
|
||||
}
|
||||
|
||||
function tex(key){
|
||||
if(texImgs[key])return texImgs[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3D sphere shading overlay (drawn on top of texture)
|
||||
function shadeSphere(ctx,x,y,r,isStar){
|
||||
if(isStar){
|
||||
// Sun: subtle glow
|
||||
const glow=ctx.createRadialGradient(x,y,r*.6,x,y,r*1.4);
|
||||
glow.addColorStop(0,'rgba(255,200,50,.15)');glow.addColorStop(.5,'rgba(255,150,20,.05)');glow.addColorStop(1,'rgba(255,100,0,0)');
|
||||
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*1.4,0,Math.PI*2);ctx.fill();
|
||||
return;
|
||||
}
|
||||
// 3D sphere shadow
|
||||
ctx.save();ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.clip();
|
||||
const shadow=ctx.createRadialGradient(x-r*.3,y-r*.3,r*.05,x+r*.15,y+r*.15,r*1.1);
|
||||
shadow.addColorStop(0,'rgba(255,255,255,.1)');shadow.addColorStop(.35,'rgba(0,0,0,0)');
|
||||
shadow.addColorStop(.7,'rgba(0,0,0,.3)');shadow.addColorStop(1,'rgba(0,0,0,.55)');
|
||||
ctx.fillStyle=shadow;ctx.fillRect(x-r,y-r,r*2,r*2);
|
||||
// Atmosphere rim
|
||||
const atmo=ctx.createRadialGradient(x,y,r*.75,x,y,r*1.02);
|
||||
atmo.addColorStop(0,'rgba(0,0,0,0)');atmo.addColorStop(.88,'rgba(200,220,255,0)');
|
||||
atmo.addColorStop(.95,'rgba(200,220,255,.12)');atmo.addColorStop(1,'rgba(200,220,255,.3)');
|
||||
ctx.fillStyle=atmo;ctx.fillRect(x-r,y-r,r*2,r*2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// STATE
|
||||
// ═══════════════════════════════════
|
||||
let simTime=new Date('2060-08-11T10:00:00Z');
|
||||
const TMN=new Date('2050-01-01T00:00Z').getTime(),TMX=new Date('2070-01-01T00:00Z').getTime(),TR=TMX-TMN;
|
||||
let rT=simTime.getTime(),tT=simTime.getTime(),af=null,style='realistic';
|
||||
let vs=[{tp:'solar_system',ck:'sun'}],hv=null,dg=false,dS=null,dL=null,dd=false,vO={x:0,y:0},zm=1;
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// CANVAS
|
||||
// ═══════════════════════════════════
|
||||
const cv=document.getElementById('map-canvas'),ctx=cv.getContext('2d'),ct=document.getElementById('map-container');
|
||||
function rs(){const r=ct.getBoundingClientRect(),d=window.devicePixelRatio||1;cv.width=r.width*d;cv.height=r.height*d;cv.style.width=r.width+'px';cv.style.height=r.height+'px';stars=null}
|
||||
window.addEventListener('resize',()=>{rs();render()});rs();
|
||||
|
||||
function or(ox,oy,rad){ctx.beginPath();ctx.arc(ox,oy,rad,0,2*Math.PI);ctx.strokeStyle='rgba(148,163,184,.12)';ctx.lineWidth=.5;ctx.setLineDash([4,8]);ctx.stroke();ctx.setLineDash([])}
|
||||
function db(x,y,r,k,lb){
|
||||
const b=fv(k);if(!b)return;
|
||||
const isStar=b.t==='star';
|
||||
const img=tex(k);
|
||||
|
||||
if(img){
|
||||
ctx.save();ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.clip();
|
||||
ctx.drawImage(img,x-r,y-r,r*2,r*2);
|
||||
ctx.restore();
|
||||
shadeSphere(ctx,x,y,r,isStar);
|
||||
}else{
|
||||
const bc=b.c||'#888';
|
||||
if(isStar){
|
||||
const glow=ctx.createRadialGradient(x,y,r*.3,x,y,r*2.5);glow.addColorStop(0,bc+'50');glow.addColorStop(.5,bc+'20');glow.addColorStop(1,bc+'00');ctx.fillStyle=glow;ctx.beginPath();ctx.arc(x,y,r*2.5,0,Math.PI*2);ctx.fill();
|
||||
}
|
||||
const g=ctx.createRadialGradient(x-r*.25,y-r*.3,0,x,y,r);
|
||||
g.addColorStop(0,'#ffffff');g.addColorStop(.25,bc);g.addColorStop(1,b.c2||'#333');
|
||||
ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();
|
||||
shadeSphere(ctx,x,y,r,isStar);
|
||||
}
|
||||
|
||||
if(k==='saturn'&&r>6){
|
||||
ctx.save();
|
||||
ctx.strokeStyle='rgba(200,180,140,.3)';ctx.lineWidth=Math.max(1,r*.06);
|
||||
ctx.beginPath();ctx.ellipse(x,y,r*1.5,r*.18,0,0,Math.PI*2);ctx.stroke();
|
||||
ctx.strokeStyle='rgba(200,180,140,.15)';ctx.lineWidth=Math.max(1,r*.04);
|
||||
ctx.beginPath();ctx.ellipse(x,y,r*1.8,r*.28,0,0,Math.PI*2);ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.fillStyle='#e2e8f0';ctx.font=`${Math.max(8.5,r*.48)}px system-ui,sans-serif`;ctx.textAlign='center';ctx.fillText(lb,x,y-r-5);
|
||||
b._x=x;b._y=y;b._r=r
|
||||
}
|
||||
|
||||
function renderSS(cx,cy,tm){
|
||||
const jv=jd(new Date(tm));
|
||||
for(const k of SO){const rad=Math.pow(ORB[k].a,.4)*100*zm;or(cx,cy,rad)}
|
||||
db(cx,cy,BD.sun.r*zm,'sun','Sun');
|
||||
for(const k of SO){const b=BD[k],p=helio(k,jv);db(cx+p.x*zm,cy-p.y*zm,b.r*zm,k,b.n)}
|
||||
}
|
||||
function renderPS(ck,cx,cy,tm){
|
||||
const c=BD[ck];if(!c)return;const jv=jd(new Date(tm)),pr=c.fr*zm;
|
||||
db(cx,cy,pr,ck,c.n);
|
||||
if(c.moons)for(const[mk,mb]of Object.entries(c.moons)){const od=mb.orb*zm*1.8;or(cx,cy,od);const an=(2*Math.PI*((jv-J2000_JD)%mb.P))/mb.P;db(cx+od*Math.cos(an),cy-od*Math.sin(an),mb.r*2.2*zm,mk,mb.n)}
|
||||
}
|
||||
function renderSB(ck,cx,cy,tm){const b=fv(ck);if(!b)return;const mx=Math.min(cx,cy)*.52,baseR=b.fr?b.fr*3:(b.r||4)*15,rr=Math.min(baseR*zm,mx);db(cx,cy,rr,ck,b.n)}
|
||||
|
||||
function render(){
|
||||
const dpr=window.devicePixelRatio||1,W=cv.width/dpr,H=cv.height/dpr;
|
||||
ctx.save();ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
if(!stars||stars.width!==cv.width||stars.height!==cv.height)stars=mkStars(cv.width,cv.height);
|
||||
ctx.drawImage(stars,0,0,W,H);
|
||||
const v=vs[vs.length-1],cX=W/2+vO.x,cY=H/2+vO.y,tm=rT;
|
||||
if(v.tp==='solar_system')renderSS(cX,cY,tm);else if(v.tp==='planet_system')renderPS(v.ck,cX,cY,tm);else renderSB(v.ck,cX,cY,tm);
|
||||
if(hv){const b=fv(hv);if(b&&b._x!=null){ctx.beginPath();ctx.arc(b._x,b._y,(b._r||4)+5,0,Math.PI*2);ctx.strokeStyle='rgba(59,130,246,.7)';ctx.lineWidth=2;ctx.stroke()}}
|
||||
ctx.restore();uip(v)
|
||||
}
|
||||
function fv(k){if(BD[k])return BD[k];for(const bk of Object.keys(BD)){const b=BD[bk];if(b.moons&&b.moons[k])return b.moons[k]}return null}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// PANEL
|
||||
// ═══════════════════════════════════
|
||||
function uip(v){
|
||||
let sk,sn;if(v.tp==='solar_system'){sk='solar_system';sn='Solar System'}else{sk=v.ck;const b=BD[v.ck];sn=b?b.n+(v.tp==='planet_system'?' System':''):v.ck}
|
||||
document.getElementById('panel-title').textContent=sn;document.getElementById('panel-scope').textContent=sn;document.getElementById('scope-label').textContent=sn;document.getElementById('scope-indicator').textContent=sn;
|
||||
const as=MOCK_ASSETS.filter(a=>a.sc.includes(sk)),ad=document.getElementById('panel-assets');
|
||||
ad.innerHTML=as.length?as.map(a=>`<div class="asset-row"><span class="name">${a.n}</span><span class="loc">${a.loc}</span></div>`).join(''):'<div style="color:var(--muted);font-size:.76rem">No assets in scope</div>';
|
||||
document.getElementById('panel-mission-count').textContent=as.filter(a=>a.loc!=='—'&&!a.loc.includes('Surface')).length;
|
||||
const ns=new Date(rT).toISOString().slice(0,10),up=MOCK_EVENTS.filter(e=>e.d>=ns).slice(0,5),ed=document.getElementById('panel-events');
|
||||
ed.innerHTML=up.length?up.map(e=>`<div class="event-row"><span class="date">${e.d}</span> — ${e.t}</div>`).join(''):'<div style="color:var(--muted);font-size:.76rem">No upcoming events</div>'
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// CLICK (drag threshold 6px)
|
||||
// ═══════════════════════════════════
|
||||
function ht(mx,my){
|
||||
const v=vs[vs.length-1],cs=[];
|
||||
if(v.tp==='solar_system'){cs.push({k:'sun'});for(const k of SO)cs.push({k})}
|
||||
else if(v.tp==='planet_system'){const c=BD[v.ck];if(c){cs.push({k:v.ck});if(c.moons)for(const mk of Object.keys(c.moons))cs.push({k:mk})}}
|
||||
else{const b=BD[v.ck];if(b)cs.push({k:v.ck})}
|
||||
for(const{k}of cs){const b=fv(k);if(!b||b._x==null)continue;const dx=mx-b._x,dy=my-b._y,hr=(b._r||4)+7;if(dx*dx+dy*dy<=hr*hr)return k}return null
|
||||
}
|
||||
cv.addEventListener('mousedown',e=>{dg=true;dS={x:e.clientX,y:e.clientY};dL={x:e.clientX,y:e.clientY};dd=false});
|
||||
window.addEventListener('mousemove',e=>{if(!dg||!dL)return;const dx=e.clientX-dS.x,dy=e.clientY-dS.y;if(Math.abs(dx)>6||Math.abs(dy)>6)dd=true;if(dd){vO.x+=e.clientX-dL.x;vO.y+=e.clientY-dL.y;dL={x:e.clientX,y:e.clientY};cv.style.cursor='grabbing';render()}});
|
||||
window.addEventListener('mouseup',()=>{dg=false;dS=null;dL=null;cv.style.cursor=hv?'pointer':'grab'});
|
||||
cv.addEventListener('click',e=>{if(dd){dd=false;return}const r=cv.getBoundingClientRect(),t=ht(e.clientX-r.left,e.clientY-r.top);if(!t)return;const v=vs[vs.length-1];if(v.tp==='solar_system'){const b=BD[t];vs.push({tp:b&&b.moons?'planet_system':'single_body',ck:t})}else if(v.tp==='planet_system'){if(t===v.ck)vs.push({tp:'single_body',ck:t});else vs.push({tp:'single_body',ck:t})}vO={x:0,y:0};ubb();render()});
|
||||
cv.addEventListener('mousemove',e=>{if(dd)return;const r=cv.getBoundingClientRect(),t=ht(e.clientX-r.left,e.clientY-r.top);if(t!==hv){hv=t;cv.style.cursor=t?'pointer':(dg?'grabbing':'grab');render()}});
|
||||
cv.addEventListener('wheel',e=>{e.preventDefault();zm=Math.max(.3,Math.min(4,zm*(e.deltaY>0?.9:1.1)));render()},{passive:false});
|
||||
function goBack(){if(vs.length>1)vs.pop();vO={x:0,y:0};zm=1;ubb();render()}
|
||||
function ubb(){document.getElementById('back-btn').classList.toggle('visible',vs.length>1)}
|
||||
function setStyle(s){style=s;document.getElementById('btn-flat').classList.toggle('active',s==='flat');document.getElementById('btn-realistic').classList.toggle('active',s==='realistic');render()}
|
||||
|
||||
// ═══════════════════════════════════
|
||||
// TIME
|
||||
// ═══════════════════════════════════
|
||||
function fmt(ms){const d=new Date(ms);return`${d.getUTCFullYear()}-${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')} ${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}`}
|
||||
function uti(){const i=document.getElementById('sim-time-input');if(i&&document.activeElement!==i)i.value=fmt(simTime.getTime());const s=document.getElementById('time-slider');if(s&&document.activeElement!==s)s.value=Math.round(((simTime.getTime()-TMN)/TR)*1000)}
|
||||
function st(ms){tT=Math.min(TMX,Math.max(TMN,ms));ani()}
|
||||
function sh(d){st(simTime.getTime()+d*DAY)}
|
||||
function rst(){st(new Date('2060-08-11T10:00:00Z').getTime())}
|
||||
function ani(){if(af)cancelAnimationFrame(af);function step(){const d=tT-rT;if(Math.abs(d)<DAY*.3){rT=tT;simTime=new Date(tT);render();uti();af=null;return}rT+=d*.18;simTime=new Date(Math.round(rT));render();uti();af=requestAnimationFrame(step)}af=requestAnimationFrame(step)}
|
||||
document.getElementById('time-slider').addEventListener('input',()=>{st(TMN+(parseInt(document.getElementById('time-slider').value)/1000)*TR)});
|
||||
document.getElementById('sim-time-input').addEventListener('keydown',e=>{if(e.key==='Enter'){const p=new Date(document.getElementById('sim-time-input').value.trim()+'Z');if(!isNaN(p.getTime()))st(p.getTime());else{const i=document.getElementById('sim-time-input');i.style.borderColor='var(--danger)';setTimeout(()=>{i.style.borderColor=''},800)}}});
|
||||
|
||||
// ═══════════════════════════════════
|
||||
rT=simTime.getTime();tT=simTime.getTime();uti();ubb();loadTextures();render();
|
||||
window.addEventListener('resize',()=>{rs();render()});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user