<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>高级弹跳小球模拟器</title>
<style>
* {
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
padding: 0;
background-color: #0a0a14;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
color: #e0e0e0;
touch-action: none;
}
#gameCanvas {
display: block;
background-color: #0a0a14;
cursor: default;
width: 100vw;
height: 100vh;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
display: flex;
flex-direction: column;
}
.btn {
background: linear-gradient(45deg, #2a2a3a, #1a1a2a);
color: #e0e0e0;
border: 2px solid #4a4a5a;
border-radius: 12px;
padding: 12px 20px;
margin: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
backdrop-filter: blur(10px);
min-width: 120px;
text-align: center;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.5);
border-color: #5a5a6a;
}
.random-btn {
background: linear-gradient(45deg, #2e7d32, #1b5e20);
}
.clear-btn {
background: linear-gradient(45deg, #c62828, #b71c1c);
}
.settings-btn {
background: linear-gradient(45deg, #1565c0, #0d47a1);
}
.info {
position: absolute;
top: 20px;
right: 20px;
color: #a0a0b0;
font-size: 14px;
text-align: right;
z-index: 100;
background: rgba(15, 15, 25, 0.8);
padding: 12px;
border-radius: 10px;
border: 1px solid #2a2a3a;
}
.instructions {
position: absolute;
bottom: 20px;
left: 20px;
color: #a0a0b0;
font-size: 14px;
z-index: 100;
background: rgba(15, 15, 25, 0.8);
padding: 15px;
border-radius: 12px;
border: 1px solid #2a2a3a;
max-width: 320px;
backdrop-filter: blur(5px);
}
.settings-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(20, 20, 35, 0.95);
border: 2px solid #3a3a4a;
border-radius: 18px;
padding: 20px;
z-index: 200;
box-shadow: 0 15px 40px rgba(0,0,0,0.6);
min-width: 300px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
backdrop-filter: blur(15px);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #3a3a4a;
}
.settings-title {
font-size: 22px;
font-weight: bold;
color: #64b5f6;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.close-btn {
background: none;
border: none;
color: #ff6b6b;
font-size: 28px;
cursor: pointer;
padding: 5px 10px;
border-radius: 8px;
transition: all 0.2s;
}
.close-btn:hover {
background: rgba(255, 107, 107, 0.2);
transform: scale(1.1);
}
.setting-group {
margin-bottom: 18px;
}
.setting-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #c0c0d0;
font-size: 14px;
}
.setting-input {
width: 100%;
padding: 10px;
border-radius: 8px;
border: 2px solid #3a3a4a;
background: #1a1a2a;
color: #e0e0e0;
font-size: 14px;
transition: all 0.3s;
}
.setting-input:focus {
outline: none;
border-color: #64b5f6;
box-shadow: 0 0 10px rgba(100, 181, 246, 0.3);
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 15px;
padding: 8px;
border-radius: 8px;
background: rgba(30, 30, 45, 0.5);
transition: background 0.3s;
}
.checkbox-group:hover {
background: rgba(40, 40, 55, 0.7);
}
.checkbox-group input {
margin-right: 10px;
width: 18px;
height: 18px;
cursor: pointer;
}
.save-btn {
width: 100%;
padding: 14px;
background: linear-gradient(45deg, #2196f3, #1976d2);
border: none;
border-radius: 10px;
color: white;
font-size: 16px;
font-weight: bold;
cursor: pointer;
margin-top: 15px;
transition: all 0.3s;
box-shadow: 0 6px 20px rgba(33, 150, 243, 0.3);
}
.save-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(33, 150, 243, 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: 22px;
height: 22px;
border-radius: 50%;
margin-left: 10px;
border: 2px solid #4a4a5a;
vertical-align: middle;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
}
.value-display {
float: right;
color: #80b0ff;
font-weight: bold;
background: rgba(40, 40, 60, 0.6);
padding: 2px 6px;
border-radius: 5px;
font-size: 13px;
}
.theme-selector {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 8px;
}
.theme-option {
padding: 10px;
border-radius: 6px;
cursor: pointer;
text-align: center;
font-size: 12px;
font-weight: bold;
transition: all 0.3s;
border: 2px solid transparent;
}
.theme-option:hover {
transform: translateY(-1px);
border-color: #64b5f6;
}
.theme-option.active {
border-color: #64b5f6;
box-shadow: 0 0 10px rgba(100, 181, 246, 0.4);
}
.theme-green { background: linear-gradient(45deg, #2e7d32, #4caf50); }
.theme-blue { background: linear-gradient(45deg, #1565c0, #2196f3); }
.theme-purple { background: linear-gradient(45deg, #4527a0, #673ab7); }
.theme-red { background: linear-gradient(45deg, #c62828, #f44336); }
.theme-orange { background: linear-gradient(45deg, #ef6c00, #ff9800); }
.theme-teal { background: linear-gradient(45deg, #00695c, #009688); }
.theme-rainbow { background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3); }
.gravity-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.mode-indicator {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 30, 45, 0.8);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
z-index: 100;
border: 1px solid #3a3a4a;
}
.drag-hint {
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 30, 45, 0.8);
padding: 6px 12px;
border-radius: 15px;
font-size: 12px;
color: #80b0ff;
z-index: 100;
border: 1px solid #3a3a4a;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
/* 移动端适配 */
@media (max-width: 768px) {
.controls {
top: 10px;
left: 10px;
}
.btn {
padding: 10px 15px;
font-size: 14px;
margin: 6px;
min-width: 100px;
}
.info {
top: 10px;
right: 10px;
font-size: 12px;
padding: 10px;
}
.instructions {
bottom: 10px;
left: 10px;
font-size: 12px;
padding: 12px;
max-width: 200px;
}
.settings-panel {
width: 95vw;
padding: 15px;
max-height: 85vh;
}
.settings-title {
font-size: 20px;
}
.theme-selector {
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.theme-option {
padding: 8px;
font-size: 11px;
}
.gravity-controls {
grid-template-columns: 1fr;
gap: 10px;
}
}
@media (max-height: 600px) and (orientation: landscape) {
.instructions {
display: none;
}
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div class="mode-indicator" id="modeIndicator">重力模式</div>
<div class="drag-hint" id="dragHint" style="display: none;">拖拽小球移动</div>
<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>
FPS: <span id="fpsCounter">60</span>
</div>
<div class="instructions">
操作说明:<br>
• 拖拽鼠标: 发射新球<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">重力强度 <span class="value-display" id="gravityValue">0.8</span></label>
<input type="range" class="setting-input" id="gravity" min="0" max="5" step="0.1" value="0.8">
</div>
<div class="setting-group">
<label class="setting-label">弹性系数 <span class="value-display" id="elasticityValue">0.9</span></label>
<input type="range" class="setting-input" id="elasticity" min="0.1" max="1" step="0.05" value="0.9">
</div>
<div class="setting-group">
<label class="setting-label">空气阻力 <span class="value-display" id="frictionValue">0.998</span></label>
<input type="range" class="setting-input" id="friction" min="0.8" max="1" step="0.001" value="0.998">
</div>
<div class="setting-group">
<label class="setting-label">小球半径 <span class="value-display" id="ballRadiusValue">25</span></label>
<input type="range" class="setting-input" id="ballRadius" min="5" max="80" step="1" value="25">
</div>
<div class="gravity-controls">
<div class="setting-group">
<label class="setting-label">重力角度 <span class="value-display" id="gravityAngleValue">90°</span></label>
<input type="range" class="setting-input" id="gravityAngle" min="0" max="360" step="1" value="90">
</div>
<div class="setting-group">
<label class="setting-label">反弹衰减 <span class="value-display" id="bounceDampingValue">0.1</span></label>
<input type="range" class="setting-input" id="bounceDamping" min="0" max="0.5" step="0.01" value="0.1">
</div>
</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="checkbox-group">
<input type="checkbox" id="gradientColors" checked>
<label class="setting-label">渐变颜色</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="soundEnabled" checked>
<label class="setting-label">启用音效</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="noGravityMode">
<label class="setting-label">无重力模式</label>
</div>
<div class="setting-group">
<label class="setting-label">颜色主题</label>
<div class="theme-selector">
<div class="theme-option theme-green" data-theme="green">自然绿</div>
<div class="theme-option theme-blue" data-theme="blue">海洋蓝</div>
<div class="theme-option theme-purple" data-theme="purple">神秘紫</div>
<div class="theme-option theme-red" data-theme="red">热情红</div>
<div class="theme-option theme-orange" data-theme="orange">活力橙</div>
<div class="theme-option theme-teal" data-theme="teal">清新青</div>
<div class="theme-option theme-rainbow" data-theme="rainbow">彩虹色</div>
</div>
</div>
<div class="setting-group">
<label class="setting-label">背景颜色</label>
<input type="color" class="setting-input" id="backgroundColor" value="#0a0a14">
<span class="color-preview" id="bgColorPreview" style="background-color: #0a0a14;"></span>
</div>
<div class="setting-group">
<label class="setting-label">默认小球颜色</label>
<input type="color" class="setting-input" id="defaultBallColor" value="#4caf50">
<span class="color-preview" id="ballColorPreview" style="background-color: #4caf50;"></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.gravityAngle = settings.gravityAngle * Math.PI / 180;
this.bounceDamping = settings.bounceDamping;
this.trail = [];
this.maxTrail = 20;
this.color = color || this.generateColor();
this.highlightColor = this.generateHighlightColor(this.color);
this.shadowColor = this.generateShadowColor(this.color);
this.isDragging = false;
this.dragOffsetX = 0;
this.dragOffsetY = 0;
this.mass = 1;
this.lastCollisionTime = 0;
this.collisionCooldown = 100;
this.resting = false;
this.restThreshold = 0.1;
}
generateColor() {
if (!settings.randomColors) {
return settings.defaultBallColor;
}
const themes = {
green: ['#1b5e20', '#2e7d32', '#388e3c', '#43a047', '#4caf50', '#66bb6a', '#81c784', '#a5d6a7', '#c8e6c9'],
blue: ['#0d47a1', '#1565c0', '#1976d2', '#1e88e5', '#2196f3', '#42a5f5', '#64b5f6', '#90caf9', '#bbdefb'],
purple: ['#311b92', '#4527a0', '#512da8', '#5e35b1', '#673ab7', '#7e57c2', '#9575cd', '#b39ddb', '#d1c4e9'],
red: ['#b71c1c', '#c62828', '#d32f2f', '#e53935', '#f44336', '#ef5350', '#ff7961', '#ffab91', '#ffcdd2'],
orange: ['#e65100', '#ef6c00', '#f57c00', '#fb8c00', '#ff9800', '#ffa726', '#ffb74d', '#ffcc80', '#ffe0b2'],
teal: ['#004d40', '#00695c', '#00796b', '#00897b', '#009688', '#26a69a', '#4db6ac', '#80cbc4', '#b2dfdb'],
rainbow: ['#ff0000', '#ff7f00', '#ffff00', '#00ff00', '#0000ff', '#4b0082', '#9400d3', '#ff1493', '#00ffff']
};
const currentTheme = themes[settings.currentTheme] || themes.green;
return currentTheme[Math.floor(Math.random() * currentTheme.length)];
}
generateHighlightColor(baseColor) {
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)})`;
}
generateShadowColor(baseColor) {
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.max(0, r - 50)}, ${Math.max(0, g - 50)}, ${Math.max(0, b - 50)})`;
}
startDrag(mouseX, mouseY) {
const dx = mouseX - this.x;
const dy = mouseY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= this.radius) {
this.isDragging = true;
this.dragOffsetX = dx;
this.dragOffsetY = dy;
this.vx = 0;
this.vy = 0;
this.resting = false;
return true;
}
return false;
}
drag(mouseX, mouseY) {
if (this.isDragging) {
this.x = mouseX - this.dragOffsetX;
this.y = mouseY - this.dragOffsetY;
return true;
}
return false;
}
endDrag() {
this.isDragging = false;
}
update(currentTime) {
if (this.isDragging) {
this.resting = false;
return;
}
// 检查是否处于静止状态
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
if (speed < this.restThreshold &&
Math.abs(this.vy) < this.restThreshold &&
this.y + this.radius >= canvas.height - 2) {
this.resting = true;
this.vx = 0;
this.vy = 0;
return;
}
// 应用重力(如果非无重力模式且不处于静止状态)
if (!settings.noGravityMode && !this.resting) {
const gravityX = Math.cos(this.gravityAngle) * this.gravity;
const gravityY = Math.sin(this.gravityAngle) * this.gravity;
this.vx += gravityX;
this.vy += gravityY;
}
// 应用空气阻力
if (!this.resting) {
this.vx *= this.friction;
this.vy *= this.friction;
}
// 更新位置
this.x += this.vx;
this.y += this.vy;
// 添加轨迹点
if (settings.showTrail && (Math.abs(this.vx) > 0.1 || Math.abs(this.vy) > 0.1)) {
this.trail.push({x: this.x, y: this.y, time: Date.now()});
if (this.trail.length > this.maxTrail) {
this.trail.shift();
}
}
// 边界碰撞检测
const now = Date.now();
if (now - this.lastCollisionTime > this.collisionCooldown && !this.resting) {
let collided = false;
if (this.x - this.radius <= 0) {
this.x = this.radius;
this.vx = -this.vx * this.elasticity;
this.vy *= (1 - this.bounceDamping);
collided = true;
} else if (this.x + this.radius >= canvas.width) {
this.x = canvas.width - this.radius;
this.vx = -this.vx * this.elasticity;
this.vy *= (1 - this.bounceDamping);
collided = true;
}
if (this.y - this.radius <= 0) {
this.y = this.radius;
this.vy = -this.vy * this.elasticity;
this.vx *= (1 - this.bounceDamping);
collided = true;
} else if (this.y + this.radius >= canvas.height) {
this.y = canvas.height - this.radius;
this.vy = -this.vy * this.elasticity;
this.vx *= (1 - this.bounceDamping);
collided = true;
// 重置静止状态
this.resting = false;
}
if (collided) {
this.lastCollisionTime = now;
if (settings.soundEnabled && audioContext) {
playBounceSound(Math.abs(this.vx) + Math.abs(this.vy));
}
}
}
}
draw(ctx) {
if (settings.showTrail) {
// 绘制轨迹(更真实的效果)
for (let i = 0; i < this.trail.length; i++) {
const alpha = i / this.trail.length * 0.4;
const trailRadius = this.radius * (i / this.trail.length) * 0.7;
const age = Date.now() - this.trail[i].time;
const ageAlpha = Math.max(0, 1 - age / 1000);
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 * ageAlpha})`;
ctx.beginPath();
ctx.arc(this.trail[i].x, this.trail[i].y, trailRadius, 0, Math.PI * 2);
ctx.fillStyle = trailColor;
ctx.fill();
}
}
}
// 绘制主球体
if (settings.gradientColors) {
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.6, this.color);
gradient.addColorStop(1, this.shadowColor);
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
// 绘制球体边缘高光
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1.5;
ctx.stroke();
// 绘制球体顶部高光点
const highlightGradient = ctx.createRadialGradient(
this.x - this.radius/4, this.y - this.radius/4, 0,
this.x - this.radius/4, this.y - this.radius/4, this.radius/3
);
highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)');
highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.beginPath();
ctx.arc(this.x - this.radius/4, this.y - this.radius/4, this.radius/3, 0, Math.PI * 2);
ctx.fillStyle = highlightGradient;
ctx.fill();
// 拖拽时的视觉反馈
if (this.isDragging) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius + 3, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 100, 0.6)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
}
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) / 8, 30);
}
}
end() {
if (this.active && this.startPos && this.endPos) {
const dx = (this.startPos.x - this.endPos.x) / 4;
const dy = (this.startPos.y - this.endPos.y) / 4;
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.7)';
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 * 4;
const endIndicatorY = this.startPos.y + Math.sin(angle) * this.power * 4;
ctx.beginPath();
ctx.moveTo(this.startPos.x, this.startPos.y);
ctx.lineTo(endIndicatorX, endIndicatorY);
ctx.strokeStyle = 'rgba(100, 255, 100, 0.7)';
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 glowGradient = ctx.createRadialGradient(
this.startPos.x, this.startPos.y, 0,
this.startPos.x, this.startPos.y, 15
);
glowGradient.addColorStop(0, 'rgba(255, 255, 100, 0.4)');
glowGradient.addColorStop(1, 'rgba(255, 255, 100, 0)');
ctx.beginPath();
ctx.arc(this.startPos.x, this.startPos.y, 15, 0, Math.PI * 2);
ctx.fillStyle = glowGradient;
ctx.fill();
}
}
}
// 音频系统
let audioContext = null;
// 创建简单的弹跳音效
function createBounceSound() {
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 创建弹跳音效函数
window.playBounceSound = function(velocity) {
if (!audioContext || !settings.soundEnabled) return;
try {
const volume = Math.min(0.3, velocity / 50);
if (volume < 0.01) return;
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
// 根据速度调整音调
const frequency = 200 + Math.min(800, velocity * 20);
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
} catch (e) {
console.log("Audio error:", e);
}
};
} catch (e) {
console.log("Audio not supported:", e);
window.playBounceSound = function() {};
}
}
// 设置配置
const defaultSettings = {
gravity: 0.8,
elasticity: 0.9,
friction: 0.998,
ballRadius: 25,
showTrail: true,
randomColors: true,
gradientColors: true,
soundEnabled: true,
noGravityMode: false,
backgroundColor: '#0a0a14',
defaultBallColor: '#4caf50',
currentTheme: 'rainbow',
gravityAngle: 90,
bounceDamping: 0.1
};
let settings = {...defaultSettings};
let balls = [];
let lastTime = 0;
let frameCount = 0;
let fps = 60;
let draggedBall = null;
// 从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 fpsCounterElement = document.getElementById('fpsCounter');
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 modeIndicator = document.getElementById('modeIndicator');
const dragHint = document.getElementById('dragHint');
// 设置面板元素
const gravityInput = document.getElementById('gravity');
const elasticityInput = document.getElementById('elasticity');
const frictionInput = document.getElementById('friction');
const ballRadiusInput = document.getElementById('ballRadius');
const gravityAngleInput = document.getElementById('gravityAngle');
const bounceDampingInput = document.getElementById('bounceDamping');
const showTrailCheckbox = document.getElementById('showTrail');
const randomColorsCheckbox = document.getElementById('randomColors');
const gradientColorsCheckbox = document.getElementById('gradientColors');
const soundEnabledCheckbox = document.getElementById('soundEnabled');
const noGravityModeCheckbox = document.getElementById('noGravityMode');
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 gravityAngleValue = document.getElementById('gravityAngleValue');
const bounceDampingValue = document.getElementById('bounceDampingValue');
const bgColorPreview = document.getElementById('bgColorPreview');
const ballColorPreview = document.getElementById('ballColorPreview');
const themeOptions = document.querySelectorAll('.theme-option');
// 设置画布大小
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', function() {
resizeCanvas();
// 重新计算所有球的位置边界
balls.forEach(ball => {
if (ball.x > canvas.width) ball.x = canvas.width - ball.radius;
if (ball.y > canvas.height) ball.y = canvas.height - ball.radius;
});
});
// 创建初始小球
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(40, 40, 60, 0.15)';
ctx.lineWidth = 1;
for (let x = 0; x < canvas.width; x += 70) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 70) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
}
// FPS计算
function updateFPS(timestamp) {
frameCount++;
if (timestamp >= lastTime + 1000) {
fps = frameCount;
frameCount = 0;
lastTime = timestamp;
fpsCounterElement.textContent = fps;
}
}
// 更新模式指示器
function updateModeIndicator() {
if (settings.noGravityMode) {
modeIndicator.textContent = "无重力模式";
modeIndicator.style.background = "rgba(30, 80, 30, 0.8)";
dragHint.style.display = "block";
} else {
const angles = {
0: "向右", 90: "向下", 180: "向左", 270: "向上"
};
const angleText = angles[settings.gravityAngle] || `${settings.gravityAngle}°`;
modeIndicator.textContent = `重力: ${angleText}`;
modeIndicator.style.background = "rgba(30, 30, 45, 0.8)";
dragHint.style.display = "block";
}
}
// 游戏循环
function gameLoop(timestamp) {
updateFPS(timestamp);
updateModeIndicator();
// 清空画布
ctx.fillStyle = settings.backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
drawGrid();
// 更新和绘制所有小球
for (let i = 0; i < balls.length; i++) {
balls[i].update(timestamp);
balls[i].draw(ctx);
}
// 绘制发射器
launcher.draw(ctx);
// 更新球数显示
ballCountElement.textContent = balls.length;
requestAnimationFrame(gameLoop);
}
// 事件监听
function handleMouseDown(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) return;
// 右键拖拽小球
if (e.button === 2) {
canvas.style.cursor = 'grabbing';
for (let i = balls.length - 1; i >= 0; i--) {
if (balls[i].startDrag(mousePos.x, mousePos.y)) {
draggedBall = balls[i];
break;
}
}
} else if (e.button === 0) {
// 左键发射
launcher.start(mousePos);
}
}
function handleMouseMove(e) {
const rect = canvas.getBoundingClientRect();
const mousePos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
if (draggedBall) {
draggedBall.drag(mousePos.x, mousePos.y);
} else {
launcher.update(mousePos);
}
}
function handleMouseUp(e) {
canvas.style.cursor = 'default';
if (e.button === 2 && draggedBall) {
draggedBall.endDrag();
draggedBall = null;
} else if (e.button === 0) {
const newBall = launcher.end();
if (newBall) {
balls.push(newBall);
if (settings.soundEnabled && audioContext) {
playBounceSound(10);
}
}
}
}
function handleTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const touchPos = {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
};
// 检查是否点击了按钮
const buttons = document.querySelectorAll('.btn');
let clickedButton = false;
buttons.forEach(btn => {
const btnRect = btn.getBoundingClientRect();
if (touch.clientX >= btnRect.left && touch.clientX <= btnRect.right &&
touch.clientY >= btnRect.top && touch.clientY <= btnRect.bottom) {
clickedButton = true;
}
});
if (clickedButton) return;
// 触摸拖拽小球
for (let i = balls.length - 1; i >= 0; i--) {
if (balls[i].startDrag(touchPos.x, touchPos.y)) {
draggedBall = balls[i];
return;
}
}
// 触摸发射
launcher.start(touchPos);
}
function handleTouchMove(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const touchPos = {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
};
if (draggedBall) {
draggedBall.drag(touchPos.x, touchPos.y);
} else {
launcher.update(touchPos);
}
}
function handleTouchEnd(e) {
if (draggedBall) {
draggedBall.endDrag();
draggedBall = null;
} else {
const newBall = launcher.end();
if (newBall) {
balls.push(newBall);
if (settings.soundEnabled && audioContext) {
playBounceSound(10);
}
}
}
}
// 添加事件监听器
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// 触摸事件
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
// 按钮事件
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) * 15,
(Math.random() - 0.5) * 15,
color
);
balls.push(newBall);
if (settings.soundEnabled && audioContext) {
playBounceSound(15);
}
});
clearBtn.addEventListener('click', () => {
balls = [];
});
// 设置面板事件
settingsBtn.addEventListener('click', () => {
// 更新设置面板的值
gravityInput.value = settings.gravity;
elasticityInput.value = settings.elasticity;
frictionInput.value = settings.friction;
ballRadiusInput.value = settings.ballRadius;
gravityAngleInput.value = settings.gravityAngle;
bounceDampingInput.value = settings.bounceDamping;
showTrailCheckbox.checked = settings.showTrail;
randomColorsCheckbox.checked = settings.randomColors;
gradientColorsCheckbox.checked = settings.gradientColors;
soundEnabledCheckbox.checked = settings.soundEnabled;
noGravityModeCheckbox.checked = settings.noGravityMode;
backgroundColorInput.value = settings.backgroundColor;
defaultBallColorInput.value = settings.defaultBallColor;
gravityValue.textContent = settings.gravity;
elasticityValue.textContent = settings.elasticity;
frictionValue.textContent = settings.friction;
ballRadiusValue.textContent = settings.ballRadius;
gravityAngleValue.textContent = settings.gravityAngle + '°';
bounceDampingValue.textContent = settings.bounceDamping;
bgColorPreview.style.backgroundColor = settings.backgroundColor;
ballColorPreview.style.backgroundColor = settings.defaultBallColor;
// 更新主题选择
themeOptions.forEach(option => {
option.classList.remove('active');
if (option.dataset.theme === settings.currentTheme) {
option.classList.add('active');
}
});
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 = parseFloat(gravityInput.value).toFixed(1);
});
elasticityInput.addEventListener('input', () => {
elasticityValue.textContent = parseFloat(elasticityInput.value).toFixed(2);
});
frictionInput.addEventListener('input', () => {
frictionValue.textContent = parseFloat(frictionInput.value).toFixed(3);
});
ballRadiusInput.addEventListener('input', () => {
ballRadiusValue.textContent = ballRadiusInput.value;
});
gravityAngleInput.addEventListener('input', () => {
gravityAngleValue.textContent = gravityAngleInput.value + '°';
});
bounceDampingInput.addEventListener('input', () => {
bounceDampingValue.textContent = parseFloat(bounceDampingInput.value).toFixed(2);
});
backgroundColorInput.addEventListener('input', () => {
bgColorPreview.style.backgroundColor = backgroundColorInput.value;
});
defaultBallColorInput.addEventListener('input', () => {
ballColorPreview.style.backgroundColor = defaultBallColorInput.value;
});
// 主题选择
themeOptions.forEach(option => {
option.addEventListener('click', () => {
themeOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
});
});
// 保存设置
saveSettingsBtn.addEventListener('click', () => {
const activeTheme = document.querySelector('.theme-option.active');
const newSettings = {
gravity: parseFloat(gravityInput.value),
elasticity: parseFloat(elasticityInput.value),
friction: parseFloat(frictionInput.value),
ballRadius: parseInt(ballRadiusInput.value),
gravityAngle: parseInt(gravityAngleInput.value),
bounceDamping: parseFloat(bounceDampingInput.value),
showTrail: showTrailCheckbox.checked,
randomColors: randomColorsCheckbox.checked,
gradientColors: gradientColorsCheckbox.checked,
soundEnabled: soundEnabledCheckbox.checked,
noGravityMode: noGravityModeCheckbox.checked,
backgroundColor: backgroundColorInput.value,
defaultBallColor: defaultBallColorInput.value,
currentTheme: activeTheme ? activeTheme.dataset.theme : 'rainbow'
};
settings = newSettings;
saveSettings();
// 更新所有球的物理参数
balls.forEach(ball => {
ball.gravity = settings.gravity;
ball.elasticity = settings.elasticity;
ball.friction = settings.friction;
ball.radius = settings.ballRadius;
ball.gravityAngle = settings.gravityAngle * Math.PI / 180;
ball.bounceDamping = settings.bounceDamping;
});
settingsPanel.style.display = 'none';
overlay.style.display = 'none';
});
// 初始化音频
createBounceSound();
// 启动游戏循环
gameLoop();
</script>
</body>
</html>
index.html
index.html