高级弹跳小球模拟器edit icon

作者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
现在支持上传本地图片了!
index.html
            
            <!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);
        }
        
        .reset-btn {
            width: 100%;
            padding: 12px;
            background: linear-gradient(45deg, #757575, #616161);
            border: none;
            border-radius: 10px;
            color: white;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            margin-top: 10px;
            transition: all 0.3s;
        }
        
        .reset-btn:hover {
            transform: translateY(-1px);
            box-shadow: 0 4px 15px rgba(117, 117, 117, 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>
        <button class="reset-btn" id="resetSettings">重置为默认设置</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 = 25;
                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.energy = 1;
                this.lastPosition = {x: x, y: y};
            }
            
            generateColor() {
                if (!settings.randomColors) {
                    return settings.defaultBallColor;
                }
                
                const themes = {
                    green: ['#2e7d32', '#388e3c', '#43a047', '#4caf50', '#66bb6a', '#81c784', '#a5d6a7'],
                    blue: ['#1565c0', '#1976d2', '#1e88e5', '#2196f3', '#42a5f5', '#64b5f6', '#90caf9'],
                    purple: ['#4527a0', '#512da8', '#5e35b1', '#673ab7', '#7e57c2', '#9575cd', '#b39ddb'],
                    red: ['#c62828', '#d32f2f', '#e53935', '#f44336', '#ef5350', '#ff7961', '#ffab91'],
                    orange: ['#ef6c00', '#f57c00', '#fb8c00', '#ff9800', '#ffa726', '#ffb74d', '#ffcc80'],
                    teal: ['#00695c', '#00796b', '#00897b', '#009688', '#26a69a', '#4db6ac', '#80cbc4'],
                    rainbow: ['#ff4444', '#ff8800', '#ffff00', '#44ff44', '#4444ff', '#8800ff', '#ff00ff']
                };
                
                const currentTheme = themes[settings.currentTheme] || themes.rainbow;
                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 + 80)}, ${Math.min(255, g + 80)}, ${Math.min(255, b + 80)})`;
            }
            
            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 - 40)}, ${Math.max(0, g - 40)}, ${Math.max(0, b - 40)})`;
            }
            
            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.energy = 1;
                    return true;
                }
                return false;
            }
            
            drag(mouseX, mouseY) {
                if (this.isDragging) {
                    this.x = mouseX - this.dragOffsetX;
                    this.y = mouseY - this.dragOffsetY;
                    this.lastPosition = {x: this.x, y: this.y};
                    return true;
                }
                return false;
            }
            
            endDrag() {
                this.isDragging = false;
            }
            
            update(currentTime) {
                if (this.isDragging) {
                    this.energy = 1;
                    return;
                }
                
                // 更新能量
                this.energy = Math.sqrt(this.vx * this.vx + this.vy * this.vy) / 10;
                
                // 应用重力(如果非无重力模式)
                if (!settings.noGravityMode) {
                    const gravityX = Math.cos(this.gravityAngle) * this.gravity;
                    const gravityY = Math.sin(this.gravityAngle) * this.gravity;
                    this.vx += gravityX;
                    this.vy += gravityY;
                }
                
                // 应用空气阻力
                this.vx *= this.friction;
                this.vy *= this.friction;
                
                // 更新位置
                this.x += this.vx;
                this.y += this.vy;
                this.lastPosition = {x: this.x, y: this.y};
                
                // 添加轨迹点
                if (settings.showTrail && (Math.abs(this.vx) > 0.05 || Math.abs(this.vy) > 0.05)) {
                    this.trail.push({x: this.x, y: this.y, time: Date.now(), energy: this.energy});
                    if (this.trail.length > this.maxTrail) {
                        this.trail.shift();
                    }
                }
                
                // 边界碰撞检测
                const now = Date.now();
                if (now - this.lastCollisionTime > this.collisionCooldown) {
                    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;
                    }
                    
                    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.5;
                        const trailRadius = this.radius * (i / this.trail.length) * 0.8;
                        const age = Date.now() - this.trail[i].time;
                        const ageAlpha = Math.max(0, 1 - age / 1500);
                        const energyFactor = Math.min(1, this.trail[i].energy * 2);
                        
                        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 * energyFactor})`;
                            
                            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.3)';
                ctx.lineWidth = 1.8;
                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/2.5
                );
                highlightGradient.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
                highlightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
                
                ctx.beginPath();
                ctx.arc(this.x - this.radius/4, this.y - this.radius/4, this.radius/2.5, 0, Math.PI * 2);
                ctx.fillStyle = highlightGradient;
                ctx.fill();
                
                // 拖拽时的视觉反馈
                if (this.isDragging) {
                    ctx.beginPath();
                    ctx.arc(this.x, this.y, this.radius + 4, 0, Math.PI * 2);
                    ctx.strokeStyle = 'rgba(255, 255, 100, 0.7)';
                    ctx.lineWidth = 3;
                    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.8)';
                    ctx.lineWidth = 4;
                    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.8)';
                    ctx.lineWidth = 3;
                    ctx.stroke();
                    
                    // 绘制起始点
                    ctx.beginPath();
                    ctx.arc(this.startPos.x, this.startPos.y, 10, 0, Math.PI * 2);
                    ctx.fillStyle = 'rgba(255, 255, 100, 0.9)';
                    ctx.fill();
                    
                    // 起始点光晕效果
                    const glowGradient = ctx.createRadialGradient(
                        this.startPos.x, this.startPos.y, 0,
                        this.startPos.x, this.startPos.y, 20
                    );
                    glowGradient.addColorStop(0, 'rgba(255, 255, 100, 0.5)');
                    glowGradient.addColorStop(1, 'rgba(255, 255, 100, 0)');
                    
                    ctx.beginPath();
                    ctx.arc(this.startPos.x, this.startPos.y, 20, 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.4, velocity / 60);
                        if (volume < 0.01) return;
                        
                        const oscillator = audioContext.createOscillator();
                        const gainNode = audioContext.createGain();
                        
                        oscillator.connect(gainNode);
                        gainNode.connect(audioContext.destination);
                        
                        // 根据速度调整音调
                        const frequency = 150 + Math.min(1000, velocity * 25);
                        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.15);
                        
                        oscillator.start(audioContext.currentTime);
                        oscillator.stop(audioContext.currentTime + 0.15);
                    } 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));
        }
        
        // 重置为默认设置
        function resetSettings() {
            settings = {...defaultSettings};
            saveSettings();
            applySettings();
            
            // 更新UI
            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');
                }
            });
        }
        
        // 应用设置
        function applySettings() {
            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;
            });
        }
        
        // 初始化设置
        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 resetSettingsBtn = document.getElementById('resetSettings');
        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();
            applySettings();
            
            settingsPanel.style.display = 'none';
            overlay.style.display = 'none';
        });
        
        // 重置设置
        resetSettingsBtn.addEventListener('click', () => {
            if (confirm('确定要重置所有设置为默认值吗?')) {
                resetSettings();
            }
        });
        
        // 初始化音频
        createBounceSound();
        
        // 启动游戏循环
        gameLoop();
    </script>
</body>
</html>
        
编辑器加载中
预览
控制台