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

429 lines
18 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/solar_system_3d · 3D 太阳系
基于 NASA JPL J2000 轨道六要素的 3D 太阳系轨道模拟器。
独立 lab:除 Three.js CDN 外不引用其他 lab 的任何资源。
服务端配置:src/server/thought_lab/labs/solar_system_3d/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>3D 太阳系轨道模拟器 - 开普勒真实轨道</title>
<style>
html, body { margin: 0; padding: 0; overflow: hidden; background: #000; height: 100%; }
#canvas-wrap { position: fixed; inset: 0; }
#info-panel {
position: fixed; top: 16px; left: 16px; z-index: 10;
width: 300px; padding: 14px 16px;
background: rgba(8, 12, 24, 0.78);
border: 1px solid rgba(90, 140, 255, 0.35);
border-radius: 10px;
color: #cfe0ff; font: 12px/1.7 "Microsoft YaHei", "PingFang SC", sans-serif;
backdrop-filter: blur(6px);
user-select: none;
}
#info-panel h1 {
margin: 0 0 8px 0; font-size: 16px; color: #fff; letter-spacing: 1px;
}
#info-panel h1 small { font-size: 11px; color: #7fa0e0; font-weight: normal; margin-left: 6px; }
#info-panel .row { display: flex; justify-content: space-between; }
#info-panel .key { color: #8fb0ff; }
#info-panel hr { border: none; border-top: 1px solid rgba(90, 140, 255, 0.25); margin: 8px 0; }
#info-panel .src { color: #7d8db0; font-size: 11px; }
#speed-wrap { margin-top: 6px; }
#speed { width: 100%; accent-color: #5a8cff; cursor: pointer; }
#sim-date { color: #ffd479; font-weight: bold; }
#load-error {
position: fixed; inset: 0; display: none; z-index: 99;
align-items: center; justify-content: center;
color: #ff8080; font: 16px/1.8 "Microsoft YaHei", sans-serif;
background: #000; text-align: center; padding: 20px;
}
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<div id="info-panel">
<h1>3D 太阳系轨道模拟器 <small>Kepler / J2000</small></h1>
<div class="row"><span class="key">左键拖拽</span><span>旋转视角(环绕观察倾斜轨道面)</span></div>
<div class="row"><span class="key">滚轮</span><span>缩放相机远近</span></div>
<div class="row"><span class="key">右键拖拽</span><span>平移视口</span></div>
<hr>
<div id="speed-wrap">
<div class="row"><span class="key">时间流速</span><span><span id="speed-val">60</span> 天/秒</span></div>
<input type="range" id="speed" min="1" max="3650" step="1" value="60">
<div class="row"><span class="key">模拟日期</span><span id="sim-date">2000-01-01</span></div>
</div>
<hr>
<div class="src">
数据来源:NASA JPL J2000 历元轨道六要素<br>
开普勒方程 M = E − e·sinE 牛顿迭代求解<br>
椭圆轨道·太阳位于焦点·近日点快/远日点慢
</div>
</div>
<div id="load-error">Three.js 加载失败。<br>请检查网络(需访问 cdnjs / jsdelivr CDN)后刷新页面。</div>
<!-- 经典全局脚本方式引入,保证 file:// 直接打开可用 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
'use strict';
// CDN 加载失败保护:避免黑屏无提示
if (typeof THREE === 'undefined' || typeof THREE.OrbitControls === 'undefined') {
document.getElementById('load-error').style.display = 'flex';
throw new Error('Three.js CDN 加载失败');
}
/* =========================================================
* 一、真实天文数据(NASA JPL,J2000 历元近似轨道要素)
* a: 半长轴(AU) e: 偏心率 i: 轨道倾角(°)
* O: 升交点黄经(°) w_bar: 近日点黄经(°) L0: J2000平黄经(°)
* T: 恒星公转周期(天)
* 论证:近点角距 w = w_bar - O ;J2000平近点角 M0 = L0 - w_bar
* =======================================================*/
/* 轨道六要素 + 世纪变化率(secular rates, 每儒略世纪, JPL Standish 1800-2050)
* ad/ed/id/Od/wd/Ld 为 a,e,i,Ω,ϖ,L 对时间(世纪)的导数,用于长期摄动演示
* axisRA/axisDec: IAU 自转轴北极指向(赤道坐标 J2000),用于轴倾角与赤道面
* spinH: 自转周期(小时, 负值=逆行) */
const PLANETS = [
{ name:'水星', a:0.38709927, e:0.20563593, i:7.00497902, O:48.33076593, w_bar:77.45779628, L0:252.25032350,
ad:0.00000037, ed:0.00001906, id:-0.00594749, Od:-0.12534081, wd:0.16047689, Ld:149472.67411175,
axisRA:281.01, axisDec:61.45, spinH:1407.6, color:0x9c9c9c, r:0.38, type:'rock' },
{ name:'金星', a:0.72333566, e:0.00677672, i:3.39467605, O:76.67984255, w_bar:131.60246718, L0:181.97909950,
ad:0.00000390, ed:-0.00004107, id:-0.00078890, Od:-0.27769418, wd:0.00268329, Ld:58517.81538729,
axisRA:272.76, axisDec:67.16, spinH:-5832.5, color:0xe6c98a, r:0.60, type:'rock' },
{ name:'地球', a:1.00000261, e:0.01671123, i:0.00001531, O:0.0, w_bar:102.93768193, L0:100.46457166,
ad:0.00000562, ed:-0.00004392, id:-0.01294668, Od:0.0, wd:0.32255575, Ld:35999.37244981,
axisRA:0.0, axisDec:90.0, spinH:23.93, color:0x4f94cd, r:0.62, type:'rock' },
{ name:'火星', a:1.52371034, e:0.09339410, i:1.84969142, O:49.55953891, w_bar:-23.94362959, L0:-4.55343205,
ad:0.00001847, ed:0.00007882, id:-0.00813131, Od:-0.29257343, wd:-0.44441088, Ld:19140.30268499,
axisRA:317.68, axisDec:52.89, spinH:24.62, color:0xd1603d, r:0.48, type:'rock' },
{ name:'木星', a:5.20288700, e:0.04838624, i:1.30439695, O:100.47390909, w_bar:14.72847983, L0:34.39644051,
ad:-0.00011607, ed:-0.00013253, id:-0.00183714, Od:0.20469106, wd:0.21252668, Ld:3034.90371757,
axisRA:268.05, axisDec:64.49, spinH:9.93, color:0xd8a56a, r:1.70, type:'gas' },
{ name:'土星', a:9.53667594, e:0.05386179, i:2.48599187, O:113.66242448, w_bar:92.59887831, L0:49.95424423,
ad:-0.00125060, ed:-0.00050524, id:0.00193609, Od:-0.28867794, wd:-0.41897216, Ld:1222.11494028,
axisRA:40.59, axisDec:83.54, spinH:10.66, color:0xe3c986, r:1.45, ring:true, type:'gas' },
{ name:'天王星', a:19.18916464, e:0.04725744, i:0.77263783, O:74.01692503, w_bar:170.95427630, L0:313.23810451,
ad:-0.00196176, ed:-0.00004397, id:-0.00242939, Od:0.04240589, wd:0.40805281, Ld:428.49512596,
axisRA:257.31, axisDec:-15.18, spinH:-17.24, color:0x7fd4d4, r:1.00, type:'gas' },
{ name:'海王星', a:30.06992276, e:0.00859048, i:1.77004347, O:131.78422574, w_bar:44.96476227, L0:-55.12002969,
ad:0.00026291, ed:0.00005105, id:0.00035372, Od:-0.00508664, wd:-0.32241464, Ld:218.48620044,
axisRA:299.36, axisDec:43.46, spinH:16.11, color:0x4666e0, r:0.98, type:'gas' },
{ name:'冥王星·矮行星', a:39.48211, e:0.24882730, i:17.14001206, O:110.30393684, w_bar:224.06891629, L0:238.92903833,
ad:0, ed:0, id:0, Od:0, wd:0, Ld:145.2,
axisRA:132.99, axisDec:-6.16, spinH:-153.3, color:0xbfa77f, r:0.30, type:'rock' }
];
const AU = 12; // 场景比例:1 AU = 12 个 Three.js 单位(仅缩放显示,几何形状不变)
const DEG = Math.PI / 180;
// J2000.0 历元。注意:严格定义是 TT 2000-01-01 12:00,与 UTC 相差约 64 秒。
// 本科普模拟忽略 TT-UTC 时差(视觉影响 << 像素级);高精度版本需处理 ΔT=TT-UTC。
const J2000_MS = Date.UTC(2000, 0, 1, 12, 0, 0);
const OBLIQ = 23.4392911 * DEG; // 黄赤交角,用于赤道->黄道转换
/* IAU 北极(赤道坐标 RA/Dec) -> 场景坐标单位矢量(自转轴方向) */
function axisToScene(ra, dec) {
const r = ra * DEG, d = dec * DEG;
const xq = Math.cos(d) * Math.cos(r), yq = Math.cos(d) * Math.sin(r), zq = Math.sin(d);
// 赤道 -> 黄道
const xe = xq;
const ye = Math.cos(OBLIQ) * yq + Math.sin(OBLIQ) * zq;
const ze = -Math.sin(OBLIQ) * yq + Math.cos(OBLIQ) * zq;
// 黄道 -> 场景 (X=x, Y=z, Z=-y)
return new THREE.Vector3(xe, ze, -ye).normalize();
}
/* 程序化行星表面纹理(使自转可见):gas=纬向条带, rock=噪斑 */
function shade(hex, f) {
const r = (hex >> 16) & 255, g = (hex >> 8) & 255, b = hex & 255;
return `rgb(${Math.min(255, r * f) | 0},${Math.min(255, g * f) | 0},${Math.min(255, b * f) | 0})`;
}
function makePlanetTexture(p) {
const c = document.createElement('canvas');
c.width = 128; c.height = 64;
const g = c.getContext('2d');
g.fillStyle = shade(p.color, 1); g.fillRect(0, 0, 128, 64);
if (p.type === 'gas') {
for (let y = 0; y < 64; y += 4) { // 纬向条带
g.fillStyle = shade(p.color, 0.8 + 0.4 * Math.abs(Math.sin(y * 0.5)));
g.fillRect(0, y, 128, 3);
}
} else {
for (let i = 0; i < 60; i++) { // 噪斑
g.fillStyle = shade(p.color, 0.7 + Math.random() * 0.6);
g.beginPath();
g.arc(Math.random() * 128, Math.random() * 64, 2 + Math.random() * 5, 0, 7);
g.fill();
}
}
const t = new THREE.CanvasTexture(c);
return t;
}
/* 给定世纪数 T,返回该时刻的轨道要素(含长期摄动)与旋转分量、平近点角 */
function elementsAt(p, T) {
const a = p.a + p.ad * T;
const e = p.e + p.ed * T;
const i = (p.i + p.id * T) * DEG;
const O = (p.O + p.Od * T) * DEG;
const wb = (p.w_bar + p.wd * T) * DEG;
const L = (p.L0 + p.Ld * T) * DEG;
const w = wb - O; // 近点角距
const M = L - wb; // 平近点角
return {
a, e, M,
cosw: Math.cos(w), sinw: Math.sin(w),
cosO: Math.cos(O), sinO: Math.sin(O),
cosi: Math.cos(i), sini: Math.sin(i)
};
}
/* =========================================================
* 二、开普勒方程求解(独立封装,牛顿迭代法)
* M = E - e*sinE => 求偏近点角 E
* =======================================================*/
function solveKepler(M, e, tol = 1e-8, maxIter = 64) {
// 归一化 M 到 [-π, π],提升收敛性
let Mn = M % (2 * Math.PI);
if (Mn > Math.PI) Mn -= 2 * Math.PI;
if (Mn < -Math.PI) Mn += 2 * Math.PI;
// 初值:低偏心率用 M,高偏心率用 π
let E = (e < 0.8) ? Mn : Math.PI;
for (let k = 0; k < maxIter; k++) {
const f = E - e * Math.sin(E) - Mn;
const fp = 1 - e * Math.cos(E);
const dE = f / fp;
E -= dE;
if (Math.abs(dE) < tol) break;
}
return E;
}
/* 由偏近点角 E 计算黄道三维坐标(AU),太阳位于椭圆焦点
* 轨道面内坐标(焦点在原点):
* x' = a(cosE - e) y' = a*sqrt(1-e^2)*sinE
* 再经 Rz(Ω)·Rx(i)·Rz(ω) 旋转到黄道系 */
function orbitalPositionAU(el, E) {
const xp = el.a * (Math.cos(E) - el.e);
const yp = el.a * Math.sqrt(1 - el.e * el.e) * Math.sin(E);
const { cosw, sinw, cosO, sinO, cosi, sini } = el;
const x = (cosO * cosw - sinO * sinw * cosi) * xp + (-cosO * sinw - sinO * cosw * cosi) * yp;
const y = (sinO * cosw + cosO * sinw * cosi) * xp + (-sinO * sinw + cosO * cosw * cosi) * yp;
const z = (sinw * sini) * xp + (cosw * sini) * yp;
return [x, y, z];
}
// 黄道坐标 -> Three.js 场景坐标(黄道面为 XZ 平面,Y 轴朝北黄极)
function toScene(xyz) {
return new THREE.Vector3(xyz[0] * AU, xyz[2] * AU, -xyz[1] * AU);
}
/* =========================================================
* 三、场景搭建
* =======================================================*/
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 30000);
camera.position.set(0, 220, 420);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.getElementById('canvas-wrap').appendChild(renderer.domElement);
// OrbitControls:左键旋转 / 滚轮缩放 / 右键平移
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 6;
controls.maxDistance = 4000;
// 光照:太阳点光源 + 微弱环境光
scene.add(new THREE.AmbientLight(0x333344, 0.6));
const sunLight = new THREE.PointLight(0xffffff, 1.6, 0, 2);
scene.add(sunLight);
// ---- 太阳(坐标原点)----
const sun = new THREE.Mesh(
new THREE.SphereGeometry(3.2, 48, 48),
new THREE.MeshBasicMaterial({ color: 0xffcc33 })
);
scene.add(sun);
// 太阳辉光 sprite
(function addSunGlow() {
const c = document.createElement('canvas');
c.width = c.height = 256;
const g = c.getContext('2d');
const grad = g.createRadialGradient(128, 128, 0, 128, 128, 128);
grad.addColorStop(0, 'rgba(255,220,120,1)');
grad.addColorStop(0.25, 'rgba(255,170,60,0.45)');
grad.addColorStop(1, 'rgba(255,140,0,0)');
g.fillStyle = grad;
g.fillRect(0, 0, 256, 256);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: new THREE.CanvasTexture(c),
blending: THREE.AdditiveBlending, depthWrite: false, transparent: true
}));
sp.scale.set(22, 22, 1);
scene.add(sp);
})();
// ---- 稀疏星空点云 ----
(function addStarfield() {
const N = 2200;
const pos = new Float32Array(N * 3);
for (let i = 0; i < N; i++) {
// 均匀球面分布,半径 6000~9000
const r = 6000 + Math.random() * 3000;
const th = Math.random() * Math.PI * 2;
const ph = Math.acos(2 * Math.random() - 1);
pos[i * 3] = r * Math.sin(ph) * Math.cos(th);
pos[i * 3 + 1] = r * Math.cos(ph);
pos[i * 3 + 2] = r * Math.sin(ph) * Math.sin(th);
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
scene.add(new THREE.Points(geo, new THREE.PointsMaterial({
color: 0xffffff, size: 1.6, sizeAttenuation: false,
transparent: true, opacity: 0.75, depthWrite: false
})));
})();
// ---- 文字标签 sprite ----
function makeLabel(text) {
const c = document.createElement('canvas');
c.width = 128; c.height = 64;
const g = c.getContext('2d');
g.font = 'bold 30px "Microsoft YaHei", sans-serif';
g.textAlign = 'center';
g.textBaseline = 'middle';
g.fillStyle = 'rgba(220,235,255,0.95)';
g.fillText(text, 64, 32);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: new THREE.CanvasTexture(c),
transparent: true, depthTest: false
}));
sp.scale.set(9, 4.5, 1);
sp.renderOrder = 5;
return sp;
}
// ---- 行星 + 各自倾斜的 3D 轨道线 ----
const ORBIT_SEG = 256;
PLANETS.forEach(p => {
p.group = new THREE.Group(); // 轨道位置
p.axisGroup = new THREE.Group(); // 自转轴倾角(本地 +Y = 自转轴)
// 由 IAU 北极指向计算轴四元数,严格绑定真实轴倾角
p.axisGroup.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), axisToScene(p.axisRA, p.axisDec));
p.group.add(p.axisGroup);
// 行星球体(视觉放大 + 程序化纹理使自转可见)
p.mesh = new THREE.Mesh(
new THREE.SphereGeometry(p.r, 32, 32),
new THREE.MeshStandardMaterial({ map: makePlanetTexture(p), roughness: 0.85, metalness: 0.05 })
);
p.axisGroup.add(p.mesh);
// 土星环:位于本地 XZ 面 = 垂直自转轴 = 土星赤道面(不再硬编码角度)
if (p.ring) {
const ring = new THREE.Mesh(
new THREE.RingGeometry(p.r * 1.4, p.r * 2.3, 64),
new THREE.MeshBasicMaterial({ color: 0xcbb98a, side: THREE.DoubleSide, transparent: true, opacity: 0.55 })
);
ring.rotation.x = Math.PI / 2;
p.axisGroup.add(ring);
}
// 名称标签(sprite 永远面向相机,距离缩放见 animate)
p.label = makeLabel(p.name);
p.label.position.set(0, p.r + 2.6, 0);
p.group.add(p.label);
scene.add(p.group);
// 轨道线:动态 BufferGeometry,每帧随长期摄动更新
p.orbitPos = new Float32Array((ORBIT_SEG + 1) * 3);
p.orbitGeom = new THREE.BufferGeometry();
p.orbitGeom.setAttribute('position', new THREE.BufferAttribute(p.orbitPos, 3).setUsage(THREE.DynamicDrawUsage));
const orbitLine = new THREE.Line(
p.orbitGeom,
new THREE.LineBasicMaterial({ color: 0x66aaff, transparent: true, opacity: 0.32 })
);
scene.add(orbitLine);
p.spinAngle = 0;
});
/* =========================================================
* 四、时间演化与动画循环
* 平近点角 M(t) = M0 + n*t (t 为自 J2000 起的模拟天数)
* 牛顿迭代求 E -> 真位置。近日点附近 dν/dt 自然变大,
* 自动满足开普勒第二定律(面积速度恒定)。
* =======================================================*/
let simDays = 0; // 自 J2000 历元起的模拟天数
let daysPerSecond = 60; // 时间流速
const speedInput = document.getElementById('speed');
const speedVal = document.getElementById('speed-val');
const simDateEl = document.getElementById('sim-date');
speedInput.addEventListener('input', () => {
daysPerSecond = parseFloat(speedInput.value);
speedVal.textContent = daysPerSecond;
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
const dDays = dt * daysPerSecond;
simDays += dDays;
const T = simDays / 36525; // 自 J2000 起的儒略世纪数(长期摄动自变量)
for (const p of PLANETS) {
const el = elementsAt(p, T); // 含世纪变化率的当前要素
const E = solveKepler(el.M, el.e);
p.group.position.copy(toScene(orbitalPositionAU(el, E)));
// 轨道线随摄动实时更新
for (let s = 0; s <= ORBIT_SEG; s++) {
const Ec = (s / ORBIT_SEG) * Math.PI * 2;
const v = toScene(orbitalPositionAU(el, Ec));
p.orbitPos[s * 3] = v.x; p.orbitPos[s * 3 + 1] = v.y; p.orbitPos[s * 3 + 2] = v.z;
}
p.orbitGeom.attributes.position.needsUpdate = true;
// 自转:角度 = 2π * (24/自转周期h) * 天数(负周期=逆行)
p.spinAngle += 2 * Math.PI * (24 / p.spinH) * dDays;
p.mesh.rotation.y = p.spinAngle;
// 标签距离缩放:视大小近似恒定,拉远不再过大
const dist = camera.position.distanceTo(p.group.position);
p.label.scale.set(dist * 0.02, dist * 0.01, 1);
}
// 模拟日期显示
const d = new Date(J2000_MS + simDays * 86400000);
simDateEl.textContent = d.toISOString().slice(0, 10);
controls.update();
renderer.render(scene, camera);
}
animate();
// 窗口自适应
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>