豆包做的
2026-06-21 18:16:25
发布于:上海
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline Universe Sandbox 修复终版</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:Microsoft YaHei,Arial}
body{overflow:hidden;background:#000;color:#fff;user-select:none;}
#canvas{display:block;width:100vw;height:100vh}
.ui-left{
position:fixed;top:10px;left:10px;z-index:10;
background:rgba(0,0,0,0.8);padding:14px;border-radius:10px;border:1px solid #4488ff;
width:260px;max-height:90vh;overflow-y:auto
}
.ui-right{
position:fixed;top:10px;right:10px;z-index:10;
background:rgba(0,0,0,0.8);padding:14px;border-radius:10px;border:1px solid #ff8844;
width:240px;max-height:90vh;overflow-y:auto;display:none
}
h3{margin:8px 0;color:#77bbff}
.ui-right h3{color:#ffaa66}
.btn{
width:100%;padding:7px;margin:4px 0;border:none;border-radius:5px;
background:#2266dd;color:#fff;cursor:pointer;font-size:14px
}
.btn-danger{background:#dd3333}
.btn-green{background:#22aa44}
.btn-orange{background:#dd7722}
.btn:hover{filter:brightness(1.2)}
.slider-box{margin:8px 0}
.slider-box label{font-size:13px;color:#ccc;display:block;margin-bottom:3px}
input[type="range"]{width:100%}
input[type="number"]{width:100%;padding:4px;background:#111;color:#fff;border:1px solid #555;border-radius:3px}
.split{height:1px;background:#335588;margin:12px 0}
textarea{width:100%;height:80px;background:#111;color:#0f0;border:1px solid #444;padding:4px;font-size:11px}
.tip{margin-top:10px;font-size:12px;color:#aaa;line-height:1.4}
.particle-layer{position:fixed;pointer-events:none;top:0;left:0;width:100vw;height:100vh;z-index:5}
</style>
</head>
<body>
<div class="ui-left">
<h3>离线宇宙沙盘</h3>
<button class="btn" id="solarSysBtn">太阳系(含小行星带)</button>
<div class="split"></div>
<h4>创建天体</h4>
<button class="btn" id="modeStar">恒星</button>
<button class="btn" id="modePlanet">行星</button>
<button class="btn" id="modeBH">黑洞</button>
<button class="btn" id="modeNeutron">中子星</button>
<button class="btn" id="modePulse">脉冲星</button>
<button class="btn" id="modeComet">彗星</button>
<button class="btn btn-green" id="btnMoon">卫星(选中行星按M)</button>
<div class="split"></div>
<div class="slider-box">
<label>时间流速 <span id="speedLabel">1.0x</span></label>
<input type="range" min="0.1" max="15" step="0.1" value="1" id="timeSlider">
</div>
<button class="btn" id="btnPause">暂停/继续</button>
<button class="btn" id="btnOrbit">显示/隐藏轨道</button>
<div class="split"></div>
<h4>JSON存档</h4>
<textarea id="jsonText"></textarea>
<button class="btn btn-orange" id="saveJson">导出存档</button>
<button class="btn btn-orange" id="loadJson">读取存档</button>
<div class="split"></div>
<button class="btn btn-danger" id="btnClear">清空全部</button>
<div class="tip">
鼠标左键拖拽空白画布生成天体<br>左键按住拖动旋转视角|滚轮缩放|右键拖动平移<br>
WASD飞行 Q/E升降 Shift加速<br>点击星球打开右侧参数面板
</div>
</div>
<div class="ui-right" id="paramPanel">
<h3>天体参数</h3>
<div class="slider-box">
<label>质量</label>
<input type="number" id="inpMass" min="10" max="20000" step="10">
</div>
<div class="slider-box">
<label>密度</label>
<input type="number" id="inpDens" min="0.1" max="100" step="0.1">
</div>
<div class="slider-box">
<label>速度倍率</label>
<input type="number" id="inpVel" min="0.01" max="5" step="0.01">
</div>
<button class="btn" id="applyParam">应用修改</button>
<button class="btn btn-danger" id="delBody">删除天体</button>
<button class="btn" id="closePanel">关闭面板</button>
</div>
<canvas id="canvas"></canvas>
<div class="particle-layer" id="particleBox"></div>
<script>
// ========== 全局配置 ==========
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W, H;
function resize(){
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
}
resize();
window.addEventListener('resize', resize);
const G = 0.07;
let timeScale = 1;
let paused = false;
let showOrbit = true;
let createType = 'planet';
let selectedBody = null;
const bodies = [];
const DRAG_THRESHOLD = 15;
// 相机控制
const cam = {x:0,y:0,z:-120,rotX:0,rotY:0,zoom:1};
const keys = {W:0,S:0,A:0,D:0,Q:0,E:0,Shift:0};
let mouseStart = null;
let dragStartWorld = null;
let mouseBtn = -1;
// 粒子系统
const particleBox = document.getElementById('particleBox');
let explosionList = [];
class Particle{
constructor(sx,sy,color){
this.x = sx;this.y = sy;
this.vx = (Math.random()-0.5)*6;
this.vy = (Math.random()-0.5)*6;
this.life = 100;
this.dom = document.createElement('div');
this.dom.style.cssText = `position:absolute;width:6px;height:6px;border-radius:50%;background:${color};opacity:1;pointer-events:none`;
particleBox.appendChild(this.dom);
}
update(){
this.life--;
if(this.life <= 0){
this.dom.remove();
return false
}
this.x += this.vx;
this.y += this.vy;
this.dom.style.left = this.x + 'px';
this.dom.style.top = this.y + 'px';
this.dom.style.opacity = this.life/100;
return true;
}
}
function spawnExplosion(x,y,color){
for(let i=0;i<70;i++) explosionList.push(new Particle(x,y,color));
}
function updateParticles(){
explosionList = explosionList.filter(p=>p.update());
}
// 屏幕2D转世界坐标
function screenToWorld(sx,sy){
return {
x: (sx - W/2) / cam.zoom + cam.x,
y: (H/2 - sy) / cam.zoom + cam.y,
z: 0
};
}
// 3D投影到画布2D
function project(x,y,z){
let dx = x - cam.x;
let dy = y - cam.y;
let dz = z - cam.z;
// Y旋转
const cosY = Math.cos(cam.rotY), sinY = Math.sin(cam.rotY);
let x1 = dx * cosY - dz * sinY;
let z1 = dx * sinY + dz * cosY;
// X旋转
const cosX = Math.cos(cam.rotX), sinX = Math.sin(cam.rotX);
let y1 = dy * cosX - z1 * sinX;
let z2 = dy * sinX + z1 * cosX;
// 透视
const fov = 220 * cam.zoom;
const scale = fov / (fov + z2);
return {
sx: W/2 + x1 * scale,
sy: H/2 - y1 * scale,
scale,
depth: z2
}
}
// 天体类
class Body{
constructor(x,y,z,vx,vy,vz,mass,density,type){
this.x = x;this.y = y;this.z = z;
this.vx = vx;this.vy = vy;this.vz = vz;
this.mass = mass;
this.density = density;
this.type = type;
this.radius = Math.pow(mass / density, 0.33);
this.trail = [];
this.trailMax = 200;
this.pulseTimer = 0;
this.tailAngle = 0;
}
// 单向引力,优化重复计算
applyGravity(target){
if(this === target) return;
const dx = target.x - this.x;
const dy = target.y - this.y;
const dz = target.z - this.z;
const distSq = dx*dx + dy*dy + dz*dz;
const dist = Math.sqrt(distSq);
const minR = this.radius + target.radius;
if(dist < minR || distSq < 0.01) return;
const force = G * this.mass * target.mass / distSq;
const ax = force * dx / dist / this.mass;
const ay = force * dy / dist / this.mass;
const az = force * dz / dist / this.mass;
this.vx += ax;
this.vy += ay;
this.vz += az;
}
collisionCheck(){
for(let i = bodies.length - 1; i >= 0; i--){
const b = bodies[i];
if(b === this) continue;
const d = Math.hypot(b.x-this.x, b.y-this.y, b.z-this.z);
if(d < this.radius + b.radius){
const p = project(this.x,this.y,this.z);
spawnExplosion(p.sx,p.sy,'#ffdd66');
let big, small;
if(this.mass >= b.mass){ big = this; small = b; }
else { big = b; small = this; }
const totalM = big.mass + small.mass;
big.vx = (big.mass*big.vx + small.mass*small.vx) / totalM;
big.vy = (big.mass*big.vy + small.mass*small.vy) / totalM;
big.vz = (big.mass*big.vz + small.mass*small.vz) / totalM;
big.mass = totalM;
big.radius = Math.pow(big.mass / big.density, 0.33);
big.trail = [];
const idx = bodies.indexOf(small);
if(idx > -1) bodies.splice(idx,1);
}
}
}
update(dt){
if(paused) return;
const dtR = dt * timeScale;
// 引力
for(const b of bodies) this.applyGravity(b);
this.collisionCheck();
// 位移
this.x += this.vx * dtR;
this.y += this.vy * dtR;
this.z += this.vz * dtR;
// 轨道轨迹
this.trail.push({x:this.x,y:this.y,z:this.z});
if(this.trail.length > this.trailMax) this.trail.shift();
if(this.type === 'pulse') this.pulseTimer += dtR;
if(this.type === 'comet') this.tailAngle = Math.atan2(-this.vy, -this.vx);
}
draw(){
const p = project(this.x,this.y,this.z);
const r = this.radius * p.scale;
if(r < 0.3 || p.depth < -1200) return;
// 绘制轨道
if(showOrbit && this.trail.length > 3){
ctx.beginPath();
for(let i=0;i<this.trail.length;i++){
const tp = project(this.trail[i].x, this.trail[i].y, this.trail[i].z);
i === 0 ? ctx.moveTo(tp.sx, tp.sy) : ctx.lineTo(tp.sx, tp.sy);
}
let col = '#88aaff80';
if(this.type === 'star') col = '#ffdd7770';
if(this.type === 'blackhole') col = '#ff332260';
if(this.type === 'comet') col = '#aaffff70';
ctx.strokeStyle = col;
ctx.lineWidth = 1;
ctx.stroke();
}
ctx.save();
// 恒星
if(this.type === 'star'){
const grad = ctx.createRadialGradient(p.sx,p.sy,0,p.sx,p.sy,r*2.2);
grad.addColorStop(0,'#fff9aa');
grad.addColorStop(0.5,'#ffdd44');
grad.addColorStop(1,'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(p.sx,p.sy,r*2.2,0,Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffee77';
}
// 黑洞+吸积盘
else if(this.type === 'blackhole'){
ctx.translate(p.sx, p.sy);
ctx.strokeStyle = 'rgba(255,60,30,0.35)';
ctx.lineWidth = r * 0.55;
ctx.beginPath();
ctx.ellipse(0,0,r*3.8,r*2,0,0,Math.PI*2);
ctx.stroke();
ctx.resetTransform();
ctx.fillStyle = '#000000';
}
// 中子星
else if(this.type === 'neutron'){
const grad = ctx.createRadialGradient(p.sx,p.sy,0,p.sx,p.sy,r*1.8);
grad.addColorStop(0,'#ffffff');
grad.addColorStop(1,'rgba(170,204,255,0.15)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(p.sx,p.sy,r*1.8,0,Math.PI*2);
ctx.fill();
ctx.fillStyle = '#cce0ff';
}
// 脉冲星
else if(this.type === 'pulse'){
const bright = Math.sin(this.pulseTimer * 6.2) * 0.35 + 0.75;
ctx.globalAlpha = bright;
const grad = ctx.createRadialGradient(p.sx,p.sy,0,p.sx,p.sy,r*2.4);
grad.addColorStop(0,'#ffffff');
grad.addColorStop(1,'rgba(255,255,255,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(p.sx,p.sy,r*2.4,0,Math.PI*2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.globalAlpha = 1;
}
// 彗星尾巴
else if(this.type === 'comet'){
ctx.save();
ctx.translate(p.sx,p.sy);
ctx.rotate(this.tailAngle);
const grad = ctx.createLinearGradient(0,0,0,-r*14);
grad.addColorStop(0,'rgba(170,255,255,0.6)');
grad.addColorStop(1,'transparent');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(-r*0.6,0);
ctx.lineTo(0,-r*14);
ctx.lineTo(r*0.6,0);
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.fillStyle = '#aaffff';
}
// 卫星
else if(this.type === 'moon'){
ctx.fillStyle = '#aaaaaa';
}
// 普通行星
else {
const clist = ['#4488ff','#44ff88','#ff6644','#aa66ff','#ffaa44'];
ctx.fillStyle = clist[Math.floor(this.mass) % clist.length];
}
// 绘制本体圆形
ctx.beginPath();
ctx.arc(p.sx,p.sy,r,0,Math.PI*2);
ctx.fill();
ctx.restore();
}
serialize(){
return {
x:this.x,y:this.y,z:this.z,
vx:this.vx,vy:this.vy,vz:this.vz,
mass:this.mass,density:this.density,type:this.type
}
}
static load(d){
return new Body(d.x,d.y,d.z,d.vx,d.vy,d.vz,d.mass,d.density,d.type);
}
}
// 生成天体
function spawnBody(startW, endW, type){
const dx = endW.x - startW.x;
const dy = endW.y - startW.y;
const velScale = 0.0052;
let mass, density = 1;
switch(type){
case 'star': mass=1200; density=1; break;
case 'blackhole': mass=4800; density=75; break;
case 'neutron': mass=2100; density=32; break;
case 'pulse': mass=2300; density=38; break;
case 'comet': mass=10; density=0.35; break;
case 'moon': mass=42; density=1.1; break;
default: mass=25+Math.random()*85; density=1.4;
}
const vx = -dx * velScale;
const vy = -dy * velScale;
bodies.push(new Body(startW.x, startW.y, startW.z, vx, vy, 0, mass, density, type));
}
// 创建卫星
function createMoon(parent){
const dist = parent.radius * 4.2 + Math.random()*8;
const ang = Math.random() * Math.PI * 2;
const x = parent.x + Math.cos(ang) * dist;
const z = parent.z + Math.sin(ang) * dist;
const spd = Math.sqrt(G * parent.mass / dist) * 0.93;
const vx = -Math.sin(ang) * spd;
const vz = Math.cos(ang) * spd;
bodies.push(new Body(x, parent.y, z, vx, parent.vy, vz, 42, 1.1, 'moon'));
}
// 太阳系预设
function buildSolarSystem(){
bodies.length = 0;
const sun = new Body(0,0,0,0,0,0,1200,1,'star');
bodies.push(sun);
const planets = [
{d:8,m:12,den:1.8},{d:13,m:22,den:1.6},
{d:18,m:28,den:1.5},{d:24,m:18,den:1.7},
{d:40,m:220,den:0.9},{d:58,m:160,den:0.8},
{d:76,m:65,den:1.1},{d:92,m:70,den:1.0}
];
for(const p of planets){
const a = Math.random() * Math.PI * 2;
const x = Math.cos(a) * p.d;
const z = Math.sin(a) * p.d;
const spd = Math.sqrt(G * sun.mass / p.d) * 0.82;
bodies.push(new Body(x,0,z,-Math.sin(a)*spd,0,Math.cos(a)*spd,p.m,p.den,'planet'));
}
// 小行星带
for(let i=0;i<150;i++){
const d = 30 + Math.random()*14;
const a = Math.random()*Math.PI*2;
const x = Math.cos(a)*d;
const z = Math.sin(a)*d;
const spd = Math.sqrt(G*sun.mass/d)*(0.72+Math.random()*0.22);
const m = 4 + Math.random()*10;
bodies.push(new Body(x,(Math.random()-0.5)*2.8,z,-Math.sin(a)*spd,0,Math.cos(a)*spd,m,2,'planet'));
}
}
function clearAll(){
bodies.length = 0;
selectedBody = null;
document.getElementById('paramPanel').style.display = 'none';
}
// 固定星空,缩放不闪烁
function drawStarBg(){
ctx.fillStyle = '#ffffff';
const seed = 8888;
for(let i=0;i<5000;i++){
const sx = (Math.sin(i * seed) * 3000) % W;
const sy = (Math.cos(i * seed + 600) * 3000) % H;
const s = Math.random() * 1.1;
ctx.globalAlpha = Math.random() * 0.45 + 0.15;
ctx.fillRect(sx, sy, s, s);
}
ctx.globalAlpha = 1;
}
// 鼠标事件重写(彻底修复拖拽冲突)
canvas.addEventListener('mousedown', e=>{
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
mouseStart = {x:mx,y:my};
mouseBtn = e.button;
dragStartWorld = screenToWorld(mx, my);
selectedBody = null;
// 拾取天体
for(const b of bodies){
const p = project(b.x,b.y,b.z);
const hitR = b.radius * p.scale * 2.5;
if(Math.hypot(mx - p.sx, my - p.sy) < hitR){
selectedBody = b;
showParam();
return;
}
}
});
canvas.addEventListener('mousemove', e=>{
if(!mouseStart) return;
const dx = e.movementX;
const dy = e.movementY;
// 左键旋转视角
if(mouseBtn === 0){
cam.rotY += dx * 0.004;
cam.rotX += dy * 0.004;
}
// 右键平移
else if(mouseBtn === 2){
cam.x -= dx / cam.zoom;
cam.y += dy / cam.zoom;
}
});
canvas.addEventListener('mouseup', e=>{
if(mouseBtn !== 0 || !mouseStart || selectedBody){
mouseStart = null;
mouseBtn = -1;
dragStartWorld = null;
return;
}
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const dragLen = Math.hypot(mx - mouseStart.x, my - mouseStart.y);
if(dragLen >= DRAG_THRESHOLD){
const endWorld = screenToWorld(mx, my);
spawnBody(dragStartWorld, endWorld, createType);
}
mouseStart = null;
mouseBtn = -1;
dragStartWorld = null;
});
canvas.addEventListener('wheel', e=>{
e.preventDefault();
cam.zoom += e.deltaY * -0.0008;
cam.zoom = Math.max(0.2, Math.min(3, cam.zoom));
});
canvas.addEventListener('contextmenu', e=>e.preventDefault());
// 键盘控制
window.addEventListener('keydown', e=>{
const k = e.key.toUpperCase();
if(k in keys) keys[k] = 1;
if(e.shiftKey) keys.Shift = 1;
if(k === 'M' && selectedBody) createMoon(selectedBody);
});
window.addEventListener('keyup', e=>{
const k = e.key.toUpperCase();
if(k in keys) keys[k] = 0;
if(!e.shiftKey) keys.Shift = 0;
});
// WASD相机移动修复反向
function updateCameraMove(dt){
const spd = dt * (keys.Shift ? 90 : 35);
const sinY = Math.sin(cam.rotY), cosY = Math.cos(cam.rotY);
if(keys.W){ cam.x += sinY * spd; cam.z -= cosY * spd; }
if(keys.S){ cam.x -= sinY * spd; cam.z += cosY * spd; }
if(keys.A){ cam.x -= cosY * spd; cam.z -= sinY * spd; }
if(keys.D){ cam.x += cosY * spd; cam.z += sinY * spd; }
if(keys.Q) cam.y -= spd;
if(keys.E) cam.y += spd;
}
// 参数面板
function showParam(){
const panel = document.getElementById('paramPanel');
panel.style.display = 'block';
document.getElementById('inpMass').value = selectedBody.mass;
document.getElementById('inpDens').value = selectedBody.density;
document.getElementById('inpVel').value = 1;
}
document.getElementById('applyParam').onclick = ()=>{
if(!selectedBody) return;
let m = Number(document.getElementById('inpMass').value);
let d = Number(document.getElementById('inpDens').value);
let vMul = Number(document.getElementById('inpVel').value);
m = Math.max(10, Math.min(20000, m));
d = Math.max(0.1, Math.min(100, d));
vMul = Math.max(0.01, Math.min(5, vMul));
selectedBody.mass = m;
selectedBody.density = d;
selectedBody.radius = Math.pow(m / d, 0.33);
selectedBody.vx *= vMul;
selectedBody.vy *= vMul;
selectedBody.vz *= vMul;
document.getElementById('inpMass').value = m;
document.getElementById('inpDens').value = d;
document.getElementById('inpVel').value = vMul;
};
document.getElementById('delBody').onclick = ()=>{
if(!selectedBody) return;
const idx = bodies.indexOf(selectedBody);
if(idx > -1) bodies.splice(idx,1);
selectedBody = null;
document.getElementById('paramPanel').style.display = 'none';
};
document.getElementById('closePanel').onclick = ()=>{
selectedBody = null;
document.getElementById('paramPanel').style.display = 'none';
};
// JSON存档
document.getElementById('saveJson').onclick = ()=>{
const data = bodies.map(b=>b.serialize());
document.getElementById('jsonText').value = JSON.stringify(data,null,2);
};
document.getElementById('loadJson').onclick = ()=>{
const txt = document.getElementById('jsonText').value.trim();
if(!txt) return alert('无存档数据');
try{
const arr = JSON.parse(txt);
clearAll();
for(const d of arr) bodies.push(Body.load(d));
}catch(err){
alert('JSON格式错误');
console.error(err);
}
};
// UI按钮
document.getElementById('solarSysBtn').onclick = buildSolarSystem;
document.getElementById('modeStar').onclick = ()=>createType='star';
document.getElementById('modePlanet').onclick = ()=>createType='planet';
document.getElementById('modeBH').onclick = ()=>createType='blackhole';
document.getElementById('modeNeutron').onclick = ()=>createType='neutron';
document.getElementById('modePulse').onclick = ()=>createType='pulse';
document.getElementById('modeComet').onclick = ()=>createType='comet';
document.getElementById('btnMoon').onclick = ()=>selectedBody?createMoon(selectedBody):alert('请先点击选中一颗行星');
document.getElementById('btnClear').onclick = clearAll;
document.getElementById('btnPause').onclick = ()=>paused=!paused;
document.getElementById('btnOrbit').onclick = ()=>showOrbit=!showOrbit;
const slider = document.getElementById('timeSlider');
const speedLabel = document.getElementById('speedLabel');
slider.oninput = ()=>{
timeScale = Number(slider.value);
speedLabel.innerText = timeScale.toFixed(1)+'x';
};
// 主循环,限制dt最大0.033防跳变
let lastTime = performance.now();
function loop(t){
requestAnimationFrame(loop);
let dt = (t - lastTime) / 1000;
dt = Math.min(dt, 0.033);
lastTime = t;
ctx.fillStyle = '#00000c';
ctx.fillRect(0,0,W,H);
drawStarBg();
updateCameraMove(dt);
// 更新物理
for(const b of bodies) b.update(dt);
// 绘制天体
for(const b of bodies) b.draw();
updateParticles();
}
requestAnimationFrame(loop);
</script>
</body>
</html>
全部评论 2
这么长
23小时前 来自 天津
0666
昨天 来自 浙江
0
























有帮助,赞一个