Files
Toolbox/public/tools/thought_lab/labs/lagrange_points/index.html
T
yangxiangyuan ea379116cc feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点
- 新增思想实验室后端路由与鉴权逻辑
- 首页新增思想实验室页签与前端渲染逻辑
- 修复中韩半导体监控标的配置错误
- 新增市场技能推送白名单过滤逻辑与单元测试
2026-08-06 13:39:48 +08:00

314 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- ============================================================
思想实验室 · 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Ω &lt; C</code> 的区域粒子永远无法进入(零速度曲线为边界)</li>
<li>五个平衡点 L1–L5:∇Ω=0。L1–L3 为鞍点(不稳定);L4/L5 虽是极大值点,但当 <code>μ &lt; 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₀&lt;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 判据 μ&lt;0.03852 → L4/L5 ${MU<0.0385208965?'<b style="color:#7ee2a8">稳定</b>':'<b style="color:#ff8080">不稳定</b>'}`;
}
buildBase(); showLP(); requestAnimationFrame(draw);
</script>
</body>
</html>