feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点 - 新增思想实验室后端路由与鉴权逻辑 - 首页新增思想实验室页签与前端渲染逻辑 - 修复中韩半导体监控标的配置错误 - 新增市场技能推送白名单过滤逻辑与单元测试
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
<!-- ============================================================
|
||||
思想实验室 · 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>
|
||||
Reference in New Issue
Block a user