space / index.html
engerl's picture
Add 2 files
25cd845 verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇宙行星球體空間扭曲現象</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
cursor: grab;
}
body.grabbing {
cursor: grabbing;
}
canvas {
display: block;
}
.info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
border-radius: 10px;
max-width: 300px;
font-family: 'Arial', sans-serif;
}
.title {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 2rem;
text-align: center;
text-shadow: 0 0 10px #4f8fff;
font-family: 'Arial', sans-serif;
}
.controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.control-btn {
background: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid #4f8fff;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
transition: all 0.3s;
}
.control-btn:hover {
background: rgba(79, 143, 255, 0.3);
}
.star {
position: absolute;
background-color: white;
border-radius: 50%;
pointer-events: none;
}
</style>
</head>
<body>
<div class="title">宇宙行星球體空間扭曲現象</div>
<div class="controls">
<button id="addPlanet" class="control-btn">添加行星</button>
<button id="reset" class="control-btn">重置模擬</button>
<button id="toggleGravity" class="control-btn">切換引力</button>
<button id="toggleDistortion" class="control-btn">切換扭曲效果</button>
<button id="resetView" class="control-btn">重置視角</button>
</div>
<div class="info-panel">
<h3 class="text-xl font-bold mb-2 text-blue-300">空間扭曲現象</h3>
<p class="text-sm mb-2">當大質量天體在時空中移動時,會造成周圍空間的彎曲和扭曲,這正是愛因斯坦廣義相對論所描述的現象。</p>
<p class="text-sm">在此模擬中,行星的運動會在其周圍產生引力井效應,導致光線偏折和空間變形。</p>
<p class="text-sm mt-2 text-blue-200">提示: 用滑鼠拖曳可以改變視角</p>
</div>
<canvas id="spaceCanvas"></canvas>
<script>
// 初始化畫布
const canvas = document.getElementById('spaceCanvas');
const ctx = canvas.getContext('2d');
// 設置畫布大小為窗口大小
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 模擬參數
const params = {
gravity: true,
distortion: true,
distortionStrength: 0.3,
friction: 0.98,
bounce: 0.7,
starCount: 200
};
// 視角控制
const view = {
x: 0,
y: 0,
scale: 1,
isDragging: false,
lastX: 0,
lastY: 0
};
// 行星類
class Planet {
constructor(x, y, radius, mass, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.mass = mass;
this.color = color;
this.vx = (Math.random() - 0.5) * 5;
this.vy = (Math.random() - 0.5) * 5;
this.rotation = 0;
this.rotationSpeed = (Math.random() - 0.5) * 0.02;
this.texture = this.createTexture(radius, color);
}
createTexture(radius, color) {
const textureCanvas = document.createElement('canvas');
const textureCtx = textureCanvas.getContext('2d');
const size = radius * 2;
textureCanvas.width = size;
textureCanvas.height = size;
// 繪製行星紋理
const gradient = textureCtx.createRadialGradient(
radius, radius, 0,
radius, radius, radius
);
gradient.addColorStop(0, lightenColor(color, 30));
gradient.addColorStop(0.7, color);
gradient.addColorStop(1, darkenColor(color, 20));
textureCtx.beginPath();
textureCtx.arc(radius, radius, radius, 0, Math.PI * 2);
textureCtx.fillStyle = gradient;
textureCtx.fill();
// 添加一些隨機的斑點紋理
for (let i = 0; i < radius / 2; i++) {
const spotRadius = Math.random() * radius / 4;
const spotX = Math.random() * size;
const spotY = Math.random() * size;
// 確保斑點在行星內部
const dist = Math.sqrt(
Math.pow(spotX - radius, 2) +
Math.pow(spotY - radius, 2)
);
if (dist < radius - spotRadius) {
const spotColor = Math.random() > 0.5 ?
lightenColor(color, 15) :
darkenColor(color, 15);
textureCtx.beginPath();
textureCtx.arc(spotX, spotY, spotRadius, 0, Math.PI * 2);
textureCtx.fillStyle = spotColor;
textureCtx.fill();
}
}
return textureCanvas;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.rotation += this.rotationSpeed;
// 邊界碰撞檢測 - 現在基於原始空間而非視口
const scaledWidth = canvas.width / view.scale;
const scaledHeight = canvas.height / view.scale;
if (this.x - this.radius < 0) {
this.x = this.radius;
this.vx = -this.vx * params.bounce;
} else if (this.x + this.radius > scaledWidth) {
this.x = scaledWidth - this.radius;
this.vx = -this.vx * params.bounce;
}
if (this.y - this.radius < 0) {
this.y = this.radius;
this.vy = -this.vy * params.bounce;
} else if (this.y + this.radius > scaledHeight) {
this.y = scaledHeight - this.radius;
this.vy = -this.vy * params.bounce;
}
// 應用摩擦力
this.vx *= params.friction;
this.vy *= params.friction;
}
draw() {
ctx.save();
// 應用視角變換
ctx.translate(view.x, view.y);
ctx.scale(view.scale, view.scale);
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
// 繪製行星
ctx.drawImage(
this.texture,
-this.radius,
-this.radius,
this.radius * 2,
this.radius * 2
);
// 繪製行星光暈
if (params.distortion) {
const gradient = ctx.createRadialGradient(
0, 0, this.radius,
0, 0, this.radius * 2
);
gradient.addColorStop(0, `rgba(${hexToRgb(this.color)}, 0.3)`);
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.beginPath();
ctx.arc(0, 0, this.radius * 2, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
}
ctx.restore();
}
applyGravity(otherPlanets) {
if (!params.gravity) return;
otherPlanets.forEach(planet => {
if (planet !== this) {
const dx = planet.x - this.x;
const dy = planet.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 避免除以零和極端力
if (distance > 5) {
const force = (planet.mass * this.mass) / (distance * distance) * 0.0001;
const fx = (dx / distance) * force;
const fy = (dy / distance) * force;
this.vx += fx / this.mass;
this.vy += fy / this.mass;
}
}
});
}
// 獲取行星在屏幕上的位置
getScreenPosition() {
return {
x: this.x * view.scale + view.x,
y: this.y * view.scale + view.y
};
}
}
// 恆星類
class Star {
constructor() {
this.x = Math.random() * (canvas.width / view.scale);
this.y = Math.random() * (canvas.height / view.scale);
this.size = Math.random() * 3;
this.brightness = Math.random();
this.color = `rgba(255, 255, 255, ${this.brightness})`;
this.speed = 0.1 + Math.random() * 0.5;
this.direction = Math.random() * Math.PI * 2;
}
update(planets) {
// 恆星受行星引力影響
if (params.gravity) {
planets.forEach(planet => {
const dx = planet.x - this.x;
const dy = planet.y - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < planet.radius * 3) {
const angle = Math.atan2(dy, dx);
const force = (planet.mass * 0.0001) / (distance * distance);
this.x -= Math.cos(angle) * force * 10;
this.y -= Math.sin(angle) * force * 10;
}
});
}
// 恆星基本運動
this.x += Math.cos(this.direction) * this.speed;
this.y += Math.sin(this.direction) * this.speed;
// 邊界檢查 - 現在基於原始空間而非視口
const scaledWidth = canvas.width / view.scale;
const scaledHeight = canvas.height / view.scale;
if (this.x < 0 || this.x > scaledWidth ||
this.y < 0 || this.y > scaledHeight) {
this.x = Math.random() * scaledWidth;
this.y = Math.random() * scaledHeight;
this.direction = Math.random() * Math.PI * 2;
}
}
draw() {
ctx.save();
ctx.translate(view.x, view.y);
ctx.scale(view.scale, view.scale);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// 工具函數
function lightenColor(color, percent) {
const num = parseInt(color.replace("#", ""), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return `#${(
0x1000000 +
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255)
).toString(16).slice(1)}`;
}
function darkenColor(color, percent) {
const num = parseInt(color.replace("#", ""), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = (num >> 8 & 0x00FF) - amt;
const B = (num & 0x0000FF) - amt;
return `#${(
0x1000000 +
(R > 0 ? (R < 255 ? R : 255) : 0) * 0x10000 +
(G > 0 ? (G < 255 ? G : 255) : 0) * 0x100 +
(B > 0 ? (B < 255 ? B : 255) : 0)
).toString(16).slice(1)}`;
}
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `${r}, ${g}, ${b}`;
}
function getRandomPlanetColor() {
const colors = [
'#4A6FA5', // 藍色
'#A54A4A', // 紅色
'#4AA54A', // 綠色
'#A54AA5', // 紫色
'#A5A54A', // 黃色
'#4AA5A5', // 青色
'#A56F4A' // 橙色
];
return colors[Math.floor(Math.random() * colors.length)];
}
// 初始化行星和恆星
let planets = [];
let stars = [];
function init() {
planets = [];
stars = [];
// 創建一些初始行星
for (let i = 0; i < 3; i++) {
const radius = 20 + Math.random() * 30;
planets.push(new Planet(
Math.random() * (canvas.width / view.scale - radius * 2) + radius,
Math.random() * (canvas.height / view.scale - radius * 2) + radius,
radius,
radius * radius * 0.1,
getRandomPlanetColor()
));
}
// 創建恆星背景
for (let i = 0; i < params.starCount; i++) {
stars.push(new Star());
}
// 重置視角
resetView();
}
// 重置視角
function resetView() {
view.x = 0;
view.y = 0;
view.scale = 1;
}
// 動畫循環
function animate() {
// 清除畫布
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 繪製扭曲網格
if (params.distortion) {
drawSpaceGrid();
}
// 更新和繪製恆星
stars.forEach(star => {
star.update(planets);
star.draw();
});
// 更新和繪製行星
planets.forEach(planet => {
planet.applyGravity(planets);
planet.update();
planet.draw();
});
requestAnimationFrame(animate);
}
// 繪製空間網格以展示扭曲效果
function drawSpaceGrid() {
const gridSize = 40;
const rows = Math.ceil(canvas.height / (gridSize * view.scale));
const cols = Math.ceil(canvas.width / (gridSize * view.scale));
ctx.save();
ctx.translate(view.x, view.y);
ctx.scale(view.scale, view.scale);
ctx.strokeStyle = 'rgba(100, 150, 255, 0.2)';
ctx.lineWidth = 1 / view.scale;
// 繪製水平線
for (let i = 0; i < rows; i++) {
const y = i * gridSize;
ctx.beginPath();
for (let j = 0; j < cols; j++) {
const x = j * gridSize;
let newX = x;
let newY = y;
// 應用行星引力扭曲
planets.forEach(planet => {
const dx = x - planet.x;
const dy = y - planet.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < planet.radius * 5) {
const strength = params.distortionStrength * (planet.mass / 100) * (1 - distance / (planet.radius * 5));
const angle = Math.atan2(dy, dx);
newX -= Math.cos(angle) * strength * 10;
newY -= Math.sin(angle) * strength * 10;
}
});
if (j === 0) {
ctx.moveTo(newX, newY);
} else {
ctx.lineTo(newX, newY);
}
}
ctx.stroke();
}
// 繪製垂直線
for (let j = 0; j < cols; j++) {
const x = j * gridSize;
ctx.beginPath();
for (let i = 0; i < rows; i++) {
const y = i * gridSize;
let newX = x;
let newY = y;
// 應用行星引力扭曲
planets.forEach(planet => {
const dx = x - planet.x;
const dy = y - planet.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < planet.radius * 5) {
const strength = params.distortionStrength * (planet.mass / 100) * (1 - distance / (planet.radius * 5));
const angle = Math.atan2(dy, dx);
newX -= Math.cos(angle) * strength * 10;
newY -= Math.sin(angle) * strength * 10;
}
});
if (i === 0) {
ctx.moveTo(newX, newY);
} else {
ctx.lineTo(newX, newY);
}
}
ctx.stroke();
}
ctx.restore();
}
// 事件監聽器
document.getElementById('addPlanet').addEventListener('click', () => {
const radius = 20 + Math.random() * 30;
planets.push(new Planet(
Math.random() * (canvas.width / view.scale - radius * 2) + radius,
Math.random() * (canvas.height / view.scale - radius * 2) + radius,
radius,
radius * radius * 0.1,
getRandomPlanetColor()
));
});
document.getElementById('reset').addEventListener('click', init);
document.getElementById('toggleGravity').addEventListener('click', () => {
params.gravity = !params.gravity;
document.getElementById('toggleGravity').textContent =
params.gravity ? '關閉引力' : '開啟引力';
});
document.getElementById('toggleDistortion').addEventListener('click', () => {
params.distortion = !params.distortion;
document.getElementById('toggleDistortion').textContent =
params.distortion ? '關閉扭曲' : '開啟扭曲';
});
document.getElementById('resetView').addEventListener('click', resetView);
// 滑鼠拖曳事件
canvas.addEventListener('mousedown', (e) => {
view.isDragging = true;
view.lastX = e.clientX;
view.lastY = e.clientY;
document.body.classList.add('grabbing');
});
canvas.addEventListener('mousemove', (e) => {
if (view.isDragging) {
const dx = e.clientX - view.lastX;
const dy = e.clientY - view.lastY;
view.x += dx;
view.y += dy;
view.lastX = e.clientX;
view.lastY = e.clientY;
}
});
canvas.addEventListener('mouseup', () => {
view.isDragging = false;
document.body.classList.remove('grabbing');
});
canvas.addEventListener('mouseleave', () => {
view.isDragging = false;
document.body.classList.remove('grabbing');
});
// 滑鼠滾輪縮放
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
// 獲取滑鼠位置相對於畫布的座標
const mouseX = e.clientX - canvas.getBoundingClientRect().left;
const mouseY = e.clientY - canvas.getBoundingClientRect().top;
// 計算滑鼠在原始空間中的位置
const worldX = (mouseX - view.x) / view.scale;
const worldY = (mouseY - view.y) / view.scale;
// 計算縮放因子
const delta = -e.deltaY;
const zoomFactor = delta > 0 ? 1.1 : 0.9;
// 限制縮放範圍
const newScale = view.scale * zoomFactor;
if (newScale > 0.1 && newScale < 5) {
view.scale = newScale;
// 調整視圖位置以保持滑鼠下的點不變
view.x = mouseX - worldX * view.scale;
view.y = mouseY - worldY * view.scale;
}
});
// 初始化並開始動畫
init();
animate();
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=engerl/space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>