<!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>
body {
margin: 0;
padding: 0;
background-color: #14141e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
color: #f0f0f0;
}
#gameCanvas {
display: block;
background-color: #14141e;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
}
.btn {
background: linear-gradient(45deg, #3c3c4e, #2a2a3a);
color: #f0f0f0;
border: 2px solid #ffffff;
border-radius: 10px;
padding: 12px 20px;
margin: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
}
.random-btn {
background: linear-gradient(45deg, #3c8c50, #2a6a3a);
}
.clear-btn {
background: linear-gradient(45deg, #dc3c3c, #a02a2a);
}
.settings-btn {
background: linear-gradient(45deg, #3c6c8c, #2a4a6a);
}
.info {
position: absolute;
top: 20px;
right: 20px;
color: #9696a0;
font-size: 14px;
text-align: right;
z-index: 100;
}
.instructions {
position: absolute;
bottom: 20px;
left: 20px;
color: #9696a0;
font-size: 14px;
z-index: 100;
background: rgba(20, 20, 30, 0.8);
padding: 15px;
border-radius: 10px;
border: 1px solid #3c3c4e;
}
.settings-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(30, 30, 45, 0.95);
border: 2px solid #5c5c7e;
border-radius: 15px;
padding: 30px;
z-index: 200;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
min-width: 400px;
backdrop-filter: blur(10px);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #3c3c4e;
}
.settings-title {
font-size: 24px;
font-weight: bold;
color: #64b5f6;
}
.close-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 24px;
cursor: pointer;
padding: 5px 10px;
border-radius: 5px;
}
.close-btn:hover {
background: rgba(255, 107, 107, 0.2);
}
.setting-group {
margin-bottom: 20px;
}
.setting-label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #b0b0c0;
}
.setting-input {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 2px solid #3c3c4e;
background: #2a2a3a;
color: #f0f0f0;
font-size: 14px;
}
.setting-input:focus {
outline: none;
border-color: #64b5f6;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.checkbox-group input {
margin-right: 10px;
width: 18px;
height: 18px;
}
.save-btn {
width: 100%;
padding: 15px;
background: linear-gradient(45deg, #64b5f6, #3c6c8c);
border: none;
border-radius: 10px;
color: white;
font-size: 18px;
font-weight: bold;
cursor: pointer;
margin-top: 20px;
transition: all 0.3s;
}
.save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 150;
display: none;
}
.color-preview {
display: inline-block;
width: 20px;
height: 20px;
border-radius: 50%;
margin-left: 10px;
border: 2px solid #5c5c7e;
vertical-align: middle;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div class="controls">
<button class="btn random-btn" id="randomBtn">随机发射</button>
<button class="btn clear-btn" id="clearBtn">清除所有</button>
<button class="btn settings-btn" id="settingsBtn">设置</button>
</div>
<div class="info">
小球数量: <span id="ballCount">1</span><br>
按住鼠标拖拽发射新球
</div>
<div class="instructions">
操作说明:<br>
• 拖拽鼠标: 发射新球<br>
• 随机发射按钮: 随机位置发射<br>
• 清除所有按钮: 移除所有球<br>
• 设置按钮: 打开设置面板
</div>
<div class="overlay" id="overlay"></div>
<div class="settings-panel" id="settingsPanel" style="display: none;">
<div class="settings-header">
<div class="settings-title">游戏设置</div>
<button class="close-btn" id="closeSettings">×</button>
</div>
<div class="setting-group">
<label class="setting-label">重力强度</label>
<input type="range" class="setting-input" id="gravity" min="0" max="2" step="0.1" value="0.5">
<span id="gravityValue">0.5</span>
</div>
<div class="setting-group">
<label class="setting-label">弹性系数</label>
<input type="range" class="setting-input" id="elasticity" min="0.1" max="1" step="0.05" value="0.85">
<span id="elasticityValue">0.85</span>
</div>
<div class="setting-group">
<label class="setting-label">空气阻力</label>
<input type="range" class="setting-input" id="friction" min="0.9" max="1" step="0.001" value="0.995">
<span id="frictionValue">0.995</span>
</div>
<div class="setting-group">
<label class="setting-label">小球半径</label>
<input type="range" class="setting-input" id="ballRadius" min="10" max="40" step="1" value="20">
<span id="ballRadiusValue">20</span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="showTrail" checked>
<label class="setting-label">显示轨迹</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="randomColors" checked>
<label class="setting-label">随机颜色</label>
</div>
<div class="setting-group">
<label class="setting-label">背景颜色</label>
<input type="color" class="setting-input" id="backgroundColor" value="#14141e">
<span class="color-preview" id="bgColorPreview" style="background-color: #14141e;"></span>
</div>
<div class="setting-group">
<label class="setting-label">默认小球颜色</label>
<input type="color" class="setting-input" id="defaultBallColor" value="#00b450">
<span class="color-preview" id="ballColorPreview" style="background-color: #00b450;"></span>
</div>
<button class="save-btn" id="saveSettings">保存设置</button>
</div>
<script>
class Ball {
constructor(x, y, vx = 0, vy = 0, color = null) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.radius = settings.ballRadius;
this.elasticity = settings.elasticity;
this.friction = settings.friction;
this.gravity = settings.gravity;
this.trail = [];
this.maxTrail = 15;
this.color = color || this.generateRandomColor();
this.highlightColor = this.generateHighlightColor(this.color);
}
generateRandomColor() {
if (!settings.randomColors) {
return settings.defaultBallColor;
}
const colors = [
'#00b450', '#ff6b6b', '#4ecdc4', '#45b7d1',
'#96ceb4', '#feca57', '#ff9ff3', '#54a0ff',
'#5f27cd', '#00d2d3', '#ff9f43', '#10ac84'
];
return colors[Math.floor(Math.random() * colors.length)];
}
generateHighlightColor(baseColor) {
// 将十六进制颜色转换为RGB并提亮
const r = parseInt(baseColor.substr(1, 2), 16);
const g = parseInt(baseColor.substr(3, 2), 16);
const b = parseInt(baseColor.substr(5, 2), 16);
return `rgb(${Math.min(255, r + 100)}, ${Math.min(255, g + 100)}, ${Math.min(255, b + 100)})`;
}
update() {
// 应用重力
this.vy += this.gravity;
// 应用空气阻力
this.vx *= this.friction;
this.vy *= this.friction;
// 更新位置
this.x += this.vx;
this.y += this.vy;
// 添加轨迹点
if (settings.showTrail) {
this.trail.push({x: this.x, y: this.y});
if (this.trail.length > this.maxTrail) {
this.trail.shift();
}
}
// 边界碰撞检测
if (this.x - this.radius <= 0) {
this.x = this.radius;
this.vx = -this.vx * this.elasticity;
} else if (this.x + this.radius >= canvas.width) {
this.x = canvas.width - this.radius;
this.vx = -this.vx * this.elasticity;
}
if (this.y - this.radius <= 0) {
this.y = this.radius;
this.vy = -this.vy * this.elasticity;
} else if (this.y + this.radius >= canvas.height) {
this.y = canvas.height - this.radius;
this.vy = -this.vy * this.elasticity;
}
}
draw(ctx) {
if (settings.showTrail) {
// 绘制轨迹
for (let i = 0; i < this.trail.length; i++) {
const alpha = i / this.trail.length * 0.6;
const trailRadius = this.radius * (i / this.trail.length) * 0.5;
// 从颜色字符串提取RGB值
const match = this.color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (match) {
const r = parseInt(match[1], 16);
const g = parseInt(match[2], 16);
const b = parseInt(match[3], 16);
const trailColor = `rgba(${r}, ${g}, ${b}, ${alpha})`;
ctx.beginPath();
ctx.arc(this.trail[i].x, this.trail[i].y, trailRadius, 0, Math.PI * 2);
ctx.fillStyle = trailColor;
ctx.fill();
}
}
}
// 绘制主球体
const gradient = ctx.createRadialGradient(
this.x - this.radius/3, this.y - this.radius/3, 0,
this.x, this.y, this.radius
);
gradient.addColorStop(0, this.highlightColor);
gradient.addColorStop(0.7, this.color);
gradient.addColorStop(1, this.darkenColor(this.color));
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// 绘制球体边缘高光
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
ctx.lineWidth = 2;
ctx.stroke();
}
darkenColor(color) {
// 将颜色变暗
const match = color.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
if (match) {
const r = Math.max(0, parseInt(match[1], 16) - 50);
const g = Math.max(0, parseInt(match[2], 16) - 50);
const b = Math.max(0, parseInt(match[3], 16) - 50);
return `rgb(${r}, ${g}, ${b})`;
}
return color;
}
}
class Launcher {
constructor() {
this.startPos = null;
this.endPos = null;
this.active = false;
this.power = 0;
}
start(pos) {
this.startPos = pos;
this.active = true;
}
update(pos) {
if (this.active) {
this.endPos = pos;
const dx = this.startPos.x - this.endPos.x;
const dy = this.startPos.y - this.endPos.y;
this.power = Math.min(Math.sqrt(dx*dx + dy*dy) / 10, 20);
}
}
end() {
if (this.active && this.startPos && this.endPos) {
const dx = (this.startPos.x - this.endPos.x) / 5;
const dy = (this.startPos.y - this.endPos.y) / 5;
const ball = new Ball(this.startPos.x, this.startPos.y, dx, dy);
this.active = false;
this.startPos = null;
this.endPos = null;
return ball;
}
this.active = false;
this.startPos = null;
this.endPos = null;
return null;
}
draw(ctx) {
if (this.active && this.startPos && this.endPos) {
// 绘制瞄准线
ctx.beginPath();
ctx.moveTo(this.startPos.x, this.startPos.y);
ctx.lineTo(this.endPos.x, this.endPos.y);
ctx.strokeStyle = 'rgba(255, 100, 100, 0.8)';
ctx.lineWidth = 3;
ctx.stroke();
// 绘制力度指示器
const angle = Math.atan2(this.endPos.y - this.startPos.y, this.endPos.x - this.startPos.x);
const endIndicatorX = this.startPos.x + Math.cos(angle) * this.power * 5;
const endIndicatorY = this.startPos.y + Math.sin(angle) * this.power * 5;
ctx.beginPath();
ctx.moveTo(this.startPos.x, this.startPos.y);
ctx.lineTo(endIndicatorX, endIndicatorY);
ctx.strokeStyle = 'rgba(100, 255, 100, 0.8)';
ctx.lineWidth = 2;
ctx.stroke();
// 绘制起始点
ctx.beginPath();
ctx.arc(this.startPos.x, this.startPos.y, 8, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255, 255, 100, 0.8)';
ctx.fill();
}
}
}
// 设置配置
const defaultSettings = {
gravity: 0.5,
elasticity: 0.85,
friction: 0.995,
ballRadius: 20,
showTrail: true,
randomColors: true,
backgroundColor: '#14141e',
defaultBallColor: '#00b450'
};
let settings = {...defaultSettings};
// 从localStorage加载设置
function loadSettings() {
const savedSettings = localStorage.getItem('ballSimulatorSettings');
if (savedSettings) {
settings = {...defaultSettings, ...JSON.parse(savedSettings)};
}
}
// 保存设置到localStorage
function saveSettings() {
localStorage.setItem('ballSimulatorSettings', JSON.stringify(settings));
}
// 初始化设置
loadSettings();
// DOM元素
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const ballCountElement = document.getElementById('ballCount');
const randomBtn = document.getElementById('randomBtn');
const clearBtn = document.getElementById('clearBtn');
const settingsBtn = document.getElementById('settingsBtn');
const settingsPanel = document.getElementById('settingsPanel');
const overlay = document.getElementById('overlay');
const closeSettings = document.getElementById('closeSettings');
const saveSettingsBtn = document.getElementById('saveSettings');
// 设置面板元素
const gravityInput = document.getElementById('gravity');
const elasticityInput = document.getElementById('elasticity');
const frictionInput = document.getElementById('friction');
const ballRadiusInput = document.getElementById('ballRadius');
const showTrailCheckbox = document.getElementById('showTrail');
const randomColorsCheckbox = document.getElementById('randomColors');
const backgroundColorInput = document.getElementById('backgroundColor');
const defaultBallColorInput = document.getElementById('defaultBallColor');
const gravityValue = document.getElementById('gravityValue');
const elasticityValue = document.getElementById('elasticityValue');
const frictionValue = document.getElementById('frictionValue');
const ballRadiusValue = document.getElementById('ballRadiusValue');
const bgColorPreview = document.getElementById('bgColorPreview');
const ballColorPreview = document.getElementById('ballColorPreview');
// 设置画布大小
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 创建初始小球
let balls = [new Ball(canvas.width/2, canvas.height/2,
(Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10)];
const launcher = new Launcher();
// 绘制网格背景
function drawGrid() {
ctx.strokeStyle = 'rgba(60, 60, 70, 0.3)';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 50) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
// 游戏循环
function gameLoop() {
// 清空画布
ctx.fillStyle = settings.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
drawGrid();
// 更新和绘制所有小球
for (let i = balls.length - 1; i >= 0; i--) {
balls[i].update();
balls[i].draw(ctx);
}
// 绘制发射器
launcher.draw(ctx);
// 更新球数显示
ballCountElement.textContent = balls.length;
requestAnimationFrame(gameLoop);
}
// 事件监听
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const mousePos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// 检查是否点击了按钮
const buttons = document.querySelectorAll('.btn');
let clickedButton = false;
buttons.forEach(btn => {
const btnRect = btn.getBoundingClientRect();
if (e.clientX >= btnRect.left && e.clientX <= btnRect.right &&
e.clientY >= btnRect.top && e.clientY <= btnRect.bottom) {
clickedButton = true;
}
});
if (!clickedButton) {
launcher.start(mousePos);
}
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mousePos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
launcher.update(mousePos);
});
canvas.addEventListener('mouseup', (e) => {
const newBall = launcher.end();
if (newBall) {
balls.push(newBall);
}
});
// 按钮事件
randomBtn.addEventListener('click', () => {
const color = settings.randomColors ? null : settings.defaultBallColor;
const newBall = new Ball(
Math.random() * (canvas.width - 200) + 100,
Math.random() * (canvas.height - 200) + 100,
(Math.random() - 0.5) * 16,
(Math.random() - 0.5) * 16,
color
);
balls.push(newBall);
});
clearBtn.addEventListener('click', () => {
balls = [];
});
// 设置面板事件
settingsBtn.addEventListener('click', () => {
// 更新设置面板的值
gravityInput.value = settings.gravity;
elasticityInput.value = settings.elasticity;
frictionInput.value = settings.friction;
ballRadiusInput.value = settings.ballRadius;
showTrailCheckbox.checked = settings.showTrail;
randomColorsCheckbox.checked = settings.randomColors;
backgroundColorInput.value = settings.backgroundColor;
defaultBallColorInput.value = settings.defaultBallColor;
gravityValue.textContent = settings.gravity;
elasticityValue.textContent = settings.elasticity;
frictionValue.textContent = settings.friction;
ballRadiusValue.textContent = settings.ballRadius;
bgColorPreview.style.backgroundColor = settings.backgroundColor;
ballColorPreview.style.backgroundColor = settings.defaultBallColor;
settingsPanel.style.display = 'block';
overlay.style.display = 'block';
});
closeSettings.addEventListener('click', () => {
settingsPanel.style.display = 'none';
overlay.style.display = 'none';
});
overlay.addEventListener('click', () => {
settingsPanel.style.display = 'none';
overlay.style.display = 'none';
});
// 设置面板实时更新显示
gravityInput.addEventListener('input', () => {
gravityValue.textContent = gravityInput.value;
});
elasticityInput.addEventListener('input', () => {
elasticityValue.textContent = elasticityInput.value;
});
frictionInput.addEventListener('input', () => {
frictionValue.textContent = frictionInput.value;
});
ballRadiusInput.addEventListener('input', () => {
ballRadiusValue.textContent = ballRadiusInput.value;
});
backgroundColorInput.addEventListener('input', () => {
bgColorPreview.style.backgroundColor = backgroundColorInput.value;
});
defaultBallColorInput.addEventListener('input', () => {
ballColorPreview.style.backgroundColor = defaultBallColorInput.value;
});
// 保存设置
saveSettingsBtn.addEventListener('click', () => {
settings = {
gravity: parseFloat(gravityInput.value),
elasticity: parseFloat(elasticityInput.value),
friction: parseFloat(frictionInput.value),
ballRadius: parseInt(ballRadiusInput.value),
showTrail: showTrailCheckbox.checked,
randomColors: randomColorsCheckbox.checked,
backgroundColor: backgroundColorInput.value,
defaultBallColor: defaultBallColorInput.value
};
saveSettings();
settingsPanel.style.display = 'none';
overlay.style.display = 'none';
// 更新所有球的物理参数
balls.forEach(ball => {
ball.gravity = settings.gravity;
ball.elasticity = settings.elasticity;
ball.friction = settings.friction;
ball.radius = settings.ballRadius;
});
});
// 启动游戏循环
gameLoop();
</script>
</body>
</html>
index.html
index.html