2d弹跳小球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">
    <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>
        
编辑器加载中
预览
控制台