feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点 - 新增思想实验室后端路由与鉴权逻辑 - 首页新增思想实验室页签与前端渲染逻辑 - 修复中韩半导体监控标的配置错误 - 新增市场技能推送白名单过滤逻辑与单元测试
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
<!-- ============================================================
|
||||
思想实验室 · labs/sun_earth_orbit · 日地轨道
|
||||
太阳-地球运行轨迹 2D 动画,演示开普勒第二定律。
|
||||
独立 lab:单文件自包含,不引用其他 lab 的任何资源。
|
||||
服务端配置:src/server/thought_lab/labs/sun_earth_orbit/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>
|
||||
* { margin: 0; padding: 0; }
|
||||
html, body { width: 100%; height: 100%; background: #05070d; overflow: hidden; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
#hud {
|
||||
position: absolute; top: 12px; left: 14px;
|
||||
color: #8fa0bd; font: 12px/1.7 "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: rgba(10,14,26,0.55); padding: 8px 12px; border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.08); pointer-events: none;
|
||||
}
|
||||
#controls {
|
||||
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; gap: 10px; align-items: center;
|
||||
background: rgba(10,14,26,0.7); padding: 10px 16px; border-radius: 12px;
|
||||
border: 1px solid rgba(255,255,255,0.10);
|
||||
font: 13px "PingFang SC", "Microsoft YaHei", sans-serif; color: #c5cfe0;
|
||||
}
|
||||
#controls button {
|
||||
background: rgba(143,179,255,0.15); border: 1px solid rgba(143,179,255,0.4);
|
||||
color: #d8e5ff; padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
#controls button:hover { background: rgba(143,179,255,0.3); }
|
||||
#controls input[type=range] { width: 110px; accent-color: #8fb3ff; }
|
||||
label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="cv"></canvas>
|
||||
<div id="hud">
|
||||
地球公转 · 开普勒第二定律(近日点快,远日点慢)<br>
|
||||
<span id="stat"></span><br>
|
||||
<span style="color:#e8b84a">⚠ 演示动画:轨道偏心率放大至 e=0.35(真实 0.0167),非真实扁率</span>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<button id="btnPause">暂停</button>
|
||||
<span>速度</span><input type="range" id="speed" min="0.2" max="5" step="0.1" value="1">
|
||||
<label><input type="checkbox" id="showOrbit" checked> 轨道线</label>
|
||||
<label><input type="checkbox" id="showTrail" checked> 拖尾</label>
|
||||
</div>
|
||||
<script>
|
||||
const cv = document.getElementById('cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
let W = 0, H = 0, cx = 0, cy = 0;
|
||||
function resize() {
|
||||
// iframe 内 clientWidth 可能在脚本执行时为 0,优先用视口尺寸
|
||||
const w = window.innerWidth || document.documentElement.clientWidth || 800;
|
||||
const h = window.innerHeight || document.documentElement.clientHeight || 600;
|
||||
W = cv.width = Math.max(1, Math.floor(w * devicePixelRatio));
|
||||
H = cv.height = Math.max(1, Math.floor(h * devicePixelRatio));
|
||||
cx = W / 2; cy = H / 2;
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
buildStars();
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
if (window.ResizeObserver) {
|
||||
new ResizeObserver(resize).observe(document.documentElement);
|
||||
}
|
||||
|
||||
// ---- 星空背景 ----
|
||||
let stars = [];
|
||||
function buildStars() {
|
||||
stars = [];
|
||||
const n = Math.floor(W * H / 9000);
|
||||
for (let i = 0; i < n; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * W, y: Math.random() * H,
|
||||
r: Math.random() * 1.2 * devicePixelRatio + 0.3,
|
||||
a: Math.random() * 0.6 + 0.15,
|
||||
tw: Math.random() * 2 + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 轨道参数(椭圆,e 夸张化便于观察)----
|
||||
const ecc = 0.35; // 离心率(真实 0.017,夸张展示)
|
||||
let baseA = 0; // 半长轴,resize 时计算
|
||||
let sunR = 0, earthR = 0, moonOrbit = 0, moonR = 0;
|
||||
function layout() {
|
||||
baseA = Math.min(W, H) * 0.36;
|
||||
sunR = Math.min(W, H) * 0.055;
|
||||
earthR = sunR * 0.32;
|
||||
moonOrbit = earthR * 2.6;
|
||||
moonR = earthR * 0.28;
|
||||
}
|
||||
|
||||
// ---- 开普勒运动:用真近点角近似,近日点加速 ----
|
||||
let M = 0; // 平近点角
|
||||
function keplerPos(theta) {
|
||||
// r = a(1-e²)/(1+e·cosθ)
|
||||
const r = baseA * (1 - ecc * ecc) / (1 + ecc * Math.cos(theta));
|
||||
return { r, x: r * Math.cos(theta), y: r * Math.sin(theta) };
|
||||
}
|
||||
function nextTheta(theta) {
|
||||
// 角速度 ∝ 1/r²(角动量守恒 → 开普勒第二定律)
|
||||
const r = baseA * (1 - ecc * ecc) / (1 + ecc * Math.cos(theta));
|
||||
const dTheta = 0.010 * Math.pow(baseA / r, 2);
|
||||
return theta + dTheta;
|
||||
}
|
||||
|
||||
let theta = Math.PI; // 从近日点出发
|
||||
let moonAng = 0;
|
||||
let paused = false;
|
||||
let speedMul = 1;
|
||||
const trail = [];
|
||||
const TRAIL_MAX = 260;
|
||||
|
||||
// ---- 绘制 ----
|
||||
function draw(t) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
// 深空底色
|
||||
ctx.fillStyle = '#05070d';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// 星空(闪烁)
|
||||
for (const s of stars) {
|
||||
const tw = 0.6 + 0.4 * Math.sin(t * 0.001 * s.tw + s.x);
|
||||
ctx.globalAlpha = s.a * tw;
|
||||
ctx.fillStyle = '#cdd8ee';
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, s.r, 0, 7);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
layout();
|
||||
// 太阳焦点偏移:椭圆焦点在原点,太阳在焦点上,轨道中心偏移 a·e
|
||||
const ocx = cx + baseA * ecc; // 轨道几何中心
|
||||
const ocy = cy;
|
||||
|
||||
// 轨道线
|
||||
if (document.getElementById('showOrbit').checked) {
|
||||
ctx.strokeStyle = 'rgba(140,160,200,0.30)';
|
||||
ctx.lineWidth = 1 * devicePixelRatio;
|
||||
ctx.setLineDash([6 * devicePixelRatio, 6 * devicePixelRatio]);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(ocx, ocy, baseA, baseA * Math.sqrt(1 - ecc * ecc), 0, 0, 7);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 位置(太阳位于原点=焦点 cx,cy)
|
||||
const p = keplerPos(theta);
|
||||
const ex = cx + p.x, ey = cy + p.y;
|
||||
|
||||
// 拖尾
|
||||
if (document.getElementById('showTrail').checked) {
|
||||
trail.push({ x: ex, y: ey });
|
||||
if (trail.length > TRAIL_MAX) trail.shift();
|
||||
for (let i = 1; i < trail.length; i++) {
|
||||
const k = i / trail.length;
|
||||
ctx.strokeStyle = `rgba(96,165,250,${k * 0.55})`;
|
||||
ctx.lineWidth = 1.6 * k * devicePixelRatio;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(trail[i - 1].x, trail[i - 1].y);
|
||||
ctx.lineTo(trail[i].x, trail[i].y);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else trail.length = 0;
|
||||
|
||||
// 太阳(光晕)
|
||||
const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, sunR * 4);
|
||||
glow.addColorStop(0, 'rgba(255,200,80,0.9)');
|
||||
glow.addColorStop(0.25, 'rgba(255,160,50,0.35)');
|
||||
glow.addColorStop(1, 'rgba(255,140,40,0)');
|
||||
ctx.fillStyle = glow;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, sunR * 4, 0, 7);
|
||||
ctx.fill();
|
||||
const sunBody = ctx.createRadialGradient(cx - sunR*0.3, cy - sunR*0.3, sunR*0.1, cx, cy, sunR);
|
||||
sunBody.addColorStop(0, '#fff7d6');
|
||||
sunBody.addColorStop(0.6, '#ffd75e');
|
||||
sunBody.addColorStop(1, '#ff9d3c');
|
||||
ctx.fillStyle = sunBody;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, sunR, 0, 7);
|
||||
ctx.fill();
|
||||
|
||||
// 地球
|
||||
const eg = ctx.createRadialGradient(ex - earthR*0.35, ey - earthR*0.35, earthR*0.1, ex, ey, earthR);
|
||||
eg.addColorStop(0, '#9ed2ff');
|
||||
eg.addColorStop(0.55, '#3b82d9');
|
||||
eg.addColorStop(1, '#1b4a8f');
|
||||
ctx.fillStyle = eg;
|
||||
ctx.beginPath();
|
||||
ctx.arc(ex, ey, earthR, 0, 7);
|
||||
ctx.fill();
|
||||
|
||||
// 月球
|
||||
const mx = ex + moonOrbit * Math.cos(moonAng);
|
||||
const my = ey + moonOrbit * Math.sin(moonAng);
|
||||
ctx.fillStyle = '#c9ccd4';
|
||||
ctx.beginPath();
|
||||
ctx.arc(mx, my, moonR, 0, 7);
|
||||
ctx.fill();
|
||||
// 月球轨道淡线
|
||||
ctx.strokeStyle = 'rgba(200,205,215,0.15)';
|
||||
ctx.lineWidth = devicePixelRatio * 0.7;
|
||||
ctx.beginPath();
|
||||
ctx.arc(ex, ey, moonOrbit, 0, 7);
|
||||
ctx.stroke();
|
||||
|
||||
// 状态(防御除零)
|
||||
const rNow = p.r;
|
||||
const peri = baseA * (1 - ecc), apo = baseA * (1 + ecc);
|
||||
const span = apo - peri;
|
||||
const phase = span > 0 ? (rNow - peri) / span : 0.5; // 0=近日点 1=远日点
|
||||
// 真实日地距离:按同一真近点角、用真实偏心率 0.0167 计算(地球轨道 a = 1 AU)
|
||||
const eccReal = 0.0167;
|
||||
const rRealAU = (1 - eccReal * eccReal) / (1 + eccReal * Math.cos(theta));
|
||||
document.getElementById('stat').textContent =
|
||||
'真实日地距离: ' + rRealAU.toFixed(3) + ' AU(近日点0.983 / 远日点1.017) · ' +
|
||||
(phase < 0.25 ? '接近近日点(快)' : phase > 0.75 ? '接近远日点(慢)' : '中途');
|
||||
|
||||
// 推进
|
||||
if (!paused) {
|
||||
const steps = Math.max(1, Math.round(speedMul * 2));
|
||||
for (let i = 0; i < steps; i++) { theta = nextTheta(theta); }
|
||||
moonAng += 0.045 * speedMul;
|
||||
}
|
||||
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
document.getElementById('btnPause').onclick = function () {
|
||||
paused = !paused;
|
||||
this.textContent = paused ? '继续' : '暂停';
|
||||
};
|
||||
document.getElementById('speed').oninput = function () {
|
||||
speedMul = parseFloat(this.value);
|
||||
};
|
||||
|
||||
resize();
|
||||
requestAnimationFrame(draw);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user