ea379116cc
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点 - 新增思想实验室后端路由与鉴权逻辑 - 首页新增思想实验室页签与前端渲染逻辑 - 修复中韩半导体监控标的配置错误 - 新增市场技能推送白名单过滤逻辑与单元测试
314 lines
15 KiB
HTML
314 lines
15 KiB
HTML
<!-- ============================================================
|
||
思想实验室 · labs/lagrange_points · 拉格朗日点
|
||
圆形限制性三体问题(CR3BP)交互演示:有效势 / 零速度曲线 / 测试粒子。
|
||
独立 lab:单文件自包含,不引用其他 lab 的任何资源。
|
||
服务端配置:src/server/thought_lab/labs/lagrange_points/config.json
|
||
============================================================ -->
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>拉格朗日点 · 限制性三体问题交互演示</title>
|
||
<style>
|
||
:root { --bg:#0a0e1a; --panel:#101830; --line:rgba(255,255,255,.10); --txt:#d8dee9; --dim:#8fa0bd; --acc:#8fb3ff; --warn:#e8b84a; }
|
||
* { margin:0; padding:0; box-sizing:border-box; }
|
||
body { background:var(--bg); color:var(--txt); font-family:"PingFang SC","Microsoft YaHei",sans-serif; padding:18px; }
|
||
h1 { font-size:21px; color:#fff; }
|
||
.sub { color:var(--dim); font-size:12.5px; margin:4px 0 14px; }
|
||
.wrap { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-start; }
|
||
#stage { position:relative; background:#05070d; border:1px solid var(--line); border-radius:12px; overflow:hidden; }
|
||
canvas#cv { display:block; cursor:crosshair; }
|
||
#hud { position:absolute; top:10px; left:12px; font-size:12px; line-height:1.7; color:var(--dim);
|
||
background:rgba(10,14,26,.62); border:1px solid var(--line); border-radius:8px; padding:8px 12px; pointer-events:none; max-width:340px; }
|
||
#hud b { color:#fff; }
|
||
.panel { width:430px; background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:16px; }
|
||
.panel h2 { font-size:14px; color:var(--acc); margin:14px 0 8px; }
|
||
.panel h2:first-child { margin-top:0; }
|
||
.panel p, .panel li { font-size:12.8px; line-height:1.75; color:#aeb8cc; }
|
||
.panel ul { padding-left:20px; }
|
||
.row { display:flex; gap:8px; align-items:center; margin:8px 0; flex-wrap:wrap; }
|
||
select, button { background:rgba(143,179,255,.12); border:1px solid rgba(143,179,255,.4); color:#d8e5ff;
|
||
padding:6px 10px; border-radius:8px; font-size:12.5px; cursor:pointer; }
|
||
button:hover, select:hover { background:rgba(143,179,255,.28); }
|
||
input[type=range] { flex:1; accent-color:var(--acc); min-width:140px; }
|
||
label { font-size:12.5px; color:var(--dim); display:flex; gap:6px; align-items:center; cursor:pointer; }
|
||
code { background:rgba(255,255,255,.08); color:#ffd479; padding:1px 6px; border-radius:4px; font-size:12px; }
|
||
table { width:100%; border-collapse:collapse; font-size:12.3px; }
|
||
td, th { border:1px solid var(--line); padding:5px 8px; text-align:left; color:#aeb8cc; }
|
||
th { color:var(--acc); background:rgba(143,179,255,.08); }
|
||
.warn { color:var(--warn); }
|
||
.mono { font-family:Consolas,monospace; font-size:12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>拉格朗日点 · 圆形限制性三体问题(CR3BP)交互演示</h1>
|
||
<div class="sub">旋转坐标系下的有效势、零速度曲线与测试粒子运动 · 全部物理来自经典力学,无虚构成分 · 点击画面可放置测试粒子</div>
|
||
|
||
<div class="wrap">
|
||
<div id="stage">
|
||
<canvas id="cv" width="820" height="700"></canvas>
|
||
<div id="hud"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>系统与控制</h2>
|
||
<div class="row">
|
||
<span style="font-size:12.5px;color:var(--dim)">天体系统</span>
|
||
<select id="mu">
|
||
<option value="0.01215" selected>地-月 μ=0.01215</option>
|
||
<option value="0.0009537">日-木 μ=0.000954</option>
|
||
<option value="0.000003003">日-地 μ=3.0e-6</option>
|
||
<option value="0.109">冥-卡 μ=0.109(L4/L5 不稳定)</option>
|
||
</select>
|
||
</div>
|
||
<div class="row">
|
||
<span style="font-size:12.5px;color:var(--dim)">雅可比常数 C</span>
|
||
<input type="range" id="cj" min="2.95" max="4.2" step="0.005" value="3.10">
|
||
<span class="mono" id="cj-val">3.100</span>
|
||
</div>
|
||
<div class="row">
|
||
<button id="p-tad4">L4 马蹄/蝌蚪</button>
|
||
<button id="p-tad5">L5 蝌蚪</button>
|
||
<button id="p-l2">L2 准轨道</button>
|
||
<button id="p-trans">L1 颈部穿越</button>
|
||
<button id="p-out">外侧偏心轨道</button>
|
||
<button id="p-clear">清除粒子</button>
|
||
</div>
|
||
<div class="row">
|
||
<label><input type="checkbox" id="tg-trail" checked> 轨迹拖尾</label>
|
||
<label><input type="checkbox" id="tg-forbid" checked> 禁行区/零速度曲线</label>
|
||
<span style="font-size:11.5px;color:var(--dim)">(白线=滑块 C 的零速度曲线,暗红=其禁行区;黄线=粒子自身 C₀ 的零速度曲线,粒子永不可越)</span>
|
||
</div>
|
||
<div class="mono" id="lp-info"></div>
|
||
|
||
<h2>物理原理(旋转坐标系,归一化 G=M=ω=a=1)</h2>
|
||
<ul>
|
||
<li>有效势:<code>Ω = ½(x²+y²) + (1−μ)/r₁ + μ/r₂</code>(离心势+双星引力势)</li>
|
||
<li>运动方程:<code>ẍ = 2ẏ + Ω/∂x</code>,<code>ÿ = −2ẋ + ∂Ω/∂y</code>(科里奥利力使轨迹弯曲)</li>
|
||
<li>唯一守恒量——雅可比常数:<code>C = 2Ω − (ẋ²+ẏ²)</code>。因 v²≥0,<code>2Ω < C</code> 的区域粒子永远无法进入(零速度曲线为边界)</li>
|
||
<li>五个平衡点 L1–L5:∇Ω=0。L1–L3 为鞍点(不稳定);L4/L5 虽是极大值点,但当 <code>μ < 0.03852</code>(Routh 判据,1875)时科里奥利力使其稳定</li>
|
||
</ul>
|
||
|
||
<h2>真实世界的拉格朗日点(非虚构)</h2>
|
||
<table>
|
||
<tr><th>点</th><th>真实驻留者</th></tr>
|
||
<tr><td>日-地 L1</td><td>SOHO、ACE、DSCOVR、WIND 太阳观测器</td></tr>
|
||
<tr><td>日-地 L2</td><td>JWST、Gaia、Planck、WMAP、Herschel</td></tr>
|
||
<tr><td>地-月 L2</td><td>中国"鹊桥"中继星(嫦娥四号,2018);ARTEMIS 探测器曾驻留地-月 L1/L2</td></tr>
|
||
<tr><td>日-木 L4/L5</td><td>木星特洛伊小行星群(已确认上万颗)</td></tr>
|
||
<tr><td>日-地 L4/L5</td><td>地球特洛伊 2010 TK7、2020 XL5;地-月 L4/L5 的 Kordylewski 尘埃云</td></tr>
|
||
<tr><td>L3</td><td>不稳定且被太阳遮挡,无已知驻留天体(科幻"反地球"不存在)</td></tr>
|
||
</table>
|
||
|
||
<h2>诚实声明</h2>
|
||
<p class="warn">演示为理想化 CR3BP:忽略其他天体摄动、太阳光压、轨道偏心率。真实任务(如 JWST 的 Lissajous/halo 轨道)需在此模型上叠加摄动与轨道维持。画面中双星在旋转系中固定,这是坐标系选择而非物理近似。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* ============ 物理内核:CR3BP(归一化单位) ============ */
|
||
let MU = 0.01215;
|
||
const R1 = (x,y)=>Math.hypot(x+MU, y);
|
||
const R2 = (x,y)=>Math.hypot(x-1+MU, y);
|
||
// 有效势 Ω 与加速度
|
||
function Omega(x,y){ return 0.5*(x*x+y*y) + (1-MU)/R1(x,y) + MU/R2(x,y); }
|
||
function acc(x,y,vx,vy){
|
||
const r1=R1(x,y), r2=R2(x,y);
|
||
const r13=r1*r1*r1, r23=r2*r2*r2;
|
||
const ax = 2*vy + x - (1-MU)*(x+MU)/r13 - MU*(x-1+MU)/r23;
|
||
const ay = -2*vx + y - (1-MU)*y/r13 - MU*y/r23;
|
||
return [ax,ay];
|
||
}
|
||
function jacobi(x,y,vx,vy){ return 2*Omega(x,y) - (vx*vx+vy*vy); }
|
||
// RK4 单步
|
||
function rk4(st, dt){
|
||
const f = s => { const a = acc(s[0],s[1],s[2],s[3]); return [s[2],s[3],a[0],a[1]]; };
|
||
const k1=f(st);
|
||
const k2=f(st.map((v,i)=>v+dt/2*k1[i]));
|
||
const k3=f(st.map((v,i)=>v+dt/2*k2[i]));
|
||
const k4=f(st.map((v,i)=>v+dt*k3[i]));
|
||
return st.map((v,i)=>v+dt/6*(k1[i]+2*k2[i]+2*k3[i]+k4[i]));
|
||
}
|
||
// 共线拉格朗日点:f(x)=∂Ω/x|_{y=0}=0 的三个根
|
||
function collinear(){
|
||
const f = x => {
|
||
const r1=Math.abs(x+MU), r2=Math.abs(x-1+MU);
|
||
return x - (1-MU)*(x+MU)/(r1*r1*r1) - MU*(x-1+MU)/(r2*r2*r2);
|
||
};
|
||
const roots=[];
|
||
const zones=[[-1.5,-MU-1e-6],[-MU+1e-6,1-MU-1e-6],[1-MU+1e-6,1.8]];
|
||
for(const [a0,b0] of zones){
|
||
let a=a0,b=b0,fa=f(a);
|
||
for(let i=0;i<200;i++){
|
||
const m=(a+b)/2, fm=f(m);
|
||
if(fa*fm<=0){ b=m; } else { a=m; fa=fm; }
|
||
}
|
||
roots.push((a+b)/2);
|
||
}
|
||
return roots; // [L3, L1, L2]
|
||
}
|
||
function lagrangePoints(){
|
||
const [l3,l1,l2]=collinear();
|
||
return { L1:[l1,0], L2:[l2,0], L3:[l3,0], L4:[0.5-MU, Math.sqrt(3)/2], L5:[0.5-MU, -Math.sqrt(3)/2] };
|
||
}
|
||
|
||
/* ============ 视口与势场底图 ============ */
|
||
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
|
||
const X0=-1.7, X1=1.9, Y0=-1.527, Y1=1.527;
|
||
const GW=410, GH=350; // 势场网格分辨率
|
||
const off=document.createElement('canvas'); off.width=GW; off.height=GH;
|
||
const octx=off.getContext('2d');
|
||
let baseCol=null, valArr=null; // 每像素底色与 2Ω 值
|
||
const w2px=(x,y)=>[ (x-X0)/(X1-X0)*cv.width, (1-(y-Y0)/(Y1-Y0))*cv.height ];
|
||
const px2w=(px,py)=>[ X0+px/cv.width*(X1-X0), Y0+(1-py/cv.height)*(Y1-Y0) ];
|
||
|
||
function palette(t){
|
||
const stops=[[6,10,34],[16,42,96],[32,96,160],[90,170,200],[240,210,140]];
|
||
const s=Math.min(0.999,Math.max(0,t))*(stops.length-1);
|
||
const i=Math.floor(s), k=s-i, A=stops[i], B=stops[i+1];
|
||
return [A[0]+(B[0]-A[0])*k, A[1]+(B[1]-A[1])*k, A[2]+(B[2]-A[2])*k];
|
||
}
|
||
function buildBase(){
|
||
const img=octx.createImageData(GW,GH);
|
||
baseCol=new Float32Array(GW*GH*3); valArr=new Float32Array(GW*GH);
|
||
for(let j=0;j<GH;j++){
|
||
const y=Y0+(1-j/GH)*(Y1-Y0);
|
||
for(let i=0;i<GW;i++){
|
||
const x=X0+(i/GW)*(X1-X0);
|
||
const v=2*Omega(x,y); valArr[j*GW+i]=v;
|
||
const t=Math.pow(Math.min(1,Math.max(0,(v-2.9)/4)),0.5);
|
||
const c=palette(t);
|
||
baseCol[(j*GW+i)*3]=c[0]; baseCol[(j*GW+i)*3+1]=c[1]; baseCol[(j*GW+i)*3+2]=c[2];
|
||
}
|
||
}
|
||
composite();
|
||
}
|
||
let CJ=3.10;
|
||
let C0p=null; // 当前粒子的雅可比常数(画其专属零速度曲线)
|
||
function composite(){
|
||
const img=octx.createImageData(GW,GH);
|
||
const showF=document.getElementById('tg-forbid').checked;
|
||
const mask=new Uint8Array(GW*GH);
|
||
for(let k=0;k<GW*GH;k++) mask[k]=valArr[k]<CJ?1:0; // 禁行区: 2Ω<C
|
||
const mask0=C0p!=null?new Uint8Array(GW*GH):null;
|
||
if(mask0) for(let k=0;k<GW*GH;k++) mask0[k]=valArr[k]<C0p?1:0;
|
||
for(let j=0;j<GH;j++)for(let i=0;i<GW;i++){
|
||
const k=j*GW+i; let r=baseCol[k*3],g=baseCol[k*3+1],b=baseCol[k*3+2];
|
||
if(showF&&mask[k]){ r=r*0.12+34; g=g*0.10+4; b=b*0.10+6; } // 暗红禁行区
|
||
// 零速度曲线:分类边界画亮线
|
||
if(showF&&(i+1<GW&&mask[k+1]!==mask[k] || j+1<GH&&mask[k+GW]!==mask[k])){ r=235;g=240;b=250; }
|
||
// 粒子自身 C0 的零速度曲线(黄线,粒子永不可越)
|
||
if(showF&&mask0&&(i+1<GW&&mask0[k+1]!==mask0[k] || j+1<GH&&mask0[k+GW]!==mask0[k])){ r=255;g=214;b=110; }
|
||
img.data[k*4]=r; img.data[k*4+1]=g; img.data[k*4+2]=b; img.data[k*4+3]=255;
|
||
}
|
||
octx.putImageData(img,0,0);
|
||
}
|
||
|
||
/* ============ 粒子与动画 ============ */
|
||
let st=null, trail=[], C0=0, pStatus='', pLabel='', hasZVC=false;
|
||
function spawn(x,y,vx,vy,label){
|
||
st=[x,y,vx,vy]; trail=[]; C0=jacobi(x,y,vx,vy); C0p=C0;
|
||
hasZVC = C0 > jacobi(LP.L4[0],LP.L4[1],0,0) + 1e-12; // C0 高于全局最小才有禁行区
|
||
pStatus='运行中'; pLabel=label;
|
||
composite();
|
||
}
|
||
const PRESETS={
|
||
'p-tad4': ()=>spawn(LP.L4[0], LP.L4[1], 0.025, 0, 'L4 蝌蚪轨道(特洛伊型)'),
|
||
'p-tad5': ()=>spawn(LP.L5[0], LP.L5[1], -0.025, 0, 'L5 蝌蚪轨道'),
|
||
'p-l2' : ()=>spawn(LP.L2[0]+0.07, 0.05, 0, -0.10, 'L2 准轨道(不稳定,会漂移)'),
|
||
'p-trans':()=>spawn(0.72, 0.015, 0, 0, 'L1 颈部穿越(混沌)'),
|
||
'p-out' : ()=>spawn(-1.32, 0.0, 0, 0.40, '外侧偏心轨道(旋转系摆线)'),
|
||
'p-clear':()=>{ st=null; trail=[]; pStatus=''; pLabel=''; C0p=null; composite(); }
|
||
};
|
||
let LP=lagrangePoints();
|
||
|
||
const DT=0.002, SUB=12;
|
||
function step(){
|
||
if(!st) return;
|
||
for(let i=0;i<SUB;i++){
|
||
st=rk4(st,DT);
|
||
const [x,y]=st;
|
||
if(R1(x,y)<0.03){ pStatus='撞入主星'; st=null; break; }
|
||
if(R2(x,y)<0.015){ pStatus='撞入伴星'; st=null; break; }
|
||
if(Math.abs(x)>4||Math.abs(y)>4){ pStatus='逃逸出系统'; st=null; break; }
|
||
}
|
||
if(st){ trail.push([st[0],st[1]]); if(trail.length>3600) trail.shift(); }
|
||
}
|
||
|
||
function draw(){
|
||
ctx.clearRect(0,0,cv.width,cv.height);
|
||
ctx.imageSmoothingEnabled=true;
|
||
ctx.drawImage(off,0,0,cv.width,cv.height);
|
||
|
||
// 双星(旋转系中固定)
|
||
const [p1x,p1y]=w2px(-MU,0), [p2x,p2y]=w2px(1-MU,0);
|
||
const g1=ctx.createRadialGradient(p1x,p1y,0,p1x,p1y,26);
|
||
g1.addColorStop(0,'#fff7d6'); g1.addColorStop(.4,'#ffd75e'); g1.addColorStop(1,'rgba(255,157,60,0)');
|
||
ctx.fillStyle=g1; ctx.beginPath(); ctx.arc(p1x,p1y,26,0,7); ctx.fill();
|
||
ctx.fillStyle='#ffd75e'; ctx.beginPath(); ctx.arc(p1x,p1y,11,0,7); ctx.fill();
|
||
ctx.fillStyle='#c9ccd4'; ctx.beginPath(); ctx.arc(p2x,p2y,5,0,7); ctx.fill();
|
||
ctx.fillStyle='#d8dee9'; ctx.font='12px sans-serif';
|
||
ctx.fillText(MU>0.05?'冥王星':'地球', p1x-14, p1y+24);
|
||
ctx.fillText(MU>0.05?'卡戎':'月球', p2x-10, p2y-10);
|
||
|
||
// 拉格朗日点标记
|
||
ctx.strokeStyle='#fff'; ctx.fillStyle='#fff'; ctx.font='bold 12px sans-serif';
|
||
for(const [name,[lx,ly]] of Object.entries(LP)){
|
||
const [px,py]=w2px(lx,ly);
|
||
ctx.beginPath(); ctx.moveTo(px-5,py); ctx.lineTo(px+5,py); ctx.moveTo(px,py-5); ctx.lineTo(px,py+5); ctx.stroke();
|
||
ctx.fillText(name, px+7, py-6);
|
||
}
|
||
|
||
// 拖尾
|
||
if(document.getElementById('tg-trail').checked && trail.length>1){
|
||
for(let i=1;i<trail.length;i++){
|
||
const a=i/trail.length;
|
||
ctx.strokeStyle=`rgba(96,200,250,${0.15+0.6*a})`;
|
||
ctx.lineWidth=1.4;
|
||
const [x1,y1]=w2px(trail[i-1][0],trail[i-1][1]), [x2,y2]=w2px(trail[i][0],trail[i][1]);
|
||
ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
|
||
}
|
||
}
|
||
// 粒子
|
||
if(st){
|
||
const [px,py]=w2px(st[0],st[1]);
|
||
const g=ctx.createRadialGradient(px,py,0,px,py,9);
|
||
g.addColorStop(0,'rgba(255,255,255,1)'); g.addColorStop(1,'rgba(120,220,255,0)');
|
||
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(px,py,9,0,7); ctx.fill();
|
||
}
|
||
|
||
// HUD
|
||
const drift = st? Math.abs(jacobi(st[0],st[1],st[2],st[3])-C0) : 0;
|
||
document.getElementById('hud').innerHTML =
|
||
`<b>旋转坐标系</b>(双星固定)· μ=${MU}<br>` +
|
||
(pLabel? `粒子:${pLabel} · C₀=${C0.toFixed(4)}${hasZVC?'(黄线为其零速度曲线)':'(C₀<C(L4),全平面无禁行区)'}<br>` : '') +
|
||
(pStatus? `状态:<b>${pStatus}</b>` + (st? ` · 雅可比漂移 |ΔC|=${drift.toExponential(2)}(RK4 精度自检)`:'') + '<br>' : '') +
|
||
`C 滑块=${CJ.toFixed(3)} · 点击画面放置静止粒子`;
|
||
step();
|
||
requestAnimationFrame(draw);
|
||
}
|
||
|
||
/* ============ 交互 ============ */
|
||
document.getElementById('mu').onchange=e=>{ MU=parseFloat(e.target.value); LP=lagrangePoints(); buildBase(); showLP(); PRESETS['p-clear'](); };
|
||
document.getElementById('cj').oninput=e=>{ CJ=parseFloat(e.target.value); document.getElementById('cj-val').textContent=CJ.toFixed(3); composite(); };
|
||
document.getElementById('tg-forbid').onchange=composite;
|
||
for(const id of Object.keys(PRESETS)) document.getElementById(id).onclick=PRESETS[id];
|
||
cv.addEventListener('click', e=>{
|
||
const r=cv.getBoundingClientRect();
|
||
const [x,y]=px2w((e.clientX-r.left)*(cv.width/r.width), (e.clientY-r.top)*(cv.height/r.height));
|
||
spawn(x,y,0,0,'自定义(旋转系静止释放)');
|
||
});
|
||
function showLP(){
|
||
const c=n=>jacobi(LP[n][0],LP[n][1],0,0);
|
||
document.getElementById('lp-info').innerHTML =
|
||
`L1 x=${LP.L1[0].toFixed(4)} (C=${c('L1').toFixed(4)}) · L2 x=${LP.L2[0].toFixed(4)} (C=${c('L2').toFixed(4)})<br>` +
|
||
`L3 x=${LP.L3[0].toFixed(4)} (C=${c('L3').toFixed(4)}) · L4/L5 C=${c('L4').toFixed(4)}<br>` +
|
||
`Routh 判据 μ<0.03852 → L4/L5 ${MU<0.0385208965?'<b style="color:#7ee2a8">稳定</b>':'<b style="color:#ff8080">不稳定</b>'}`;
|
||
}
|
||
buildBase(); showLP(); requestAnimationFrame(draw);
|
||
</script>
|
||
</body>
|
||
</html>
|