火花秀摸拟器[支持多设备+智能优化版本]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>
    <meta name="description" content="体验壮观的烟花表演,自定义设置。支持多语言、多种烟花类型、音效和PWA功能。">
    <link rel="manifest" href="manifest.json">
    <meta name="theme-color" content="#302b63">
    <style>
        :root {
            /* 深色主题变量 */
            --bg-gradient-start: #0f0c29;
            --bg-gradient-middle: #302b63;
            --bg-gradient-end: #24243e;
            --canvas-bg: radial-gradient(circle at center, #0a0a1a 0%, #000000 70%);
            --text-color: white;
            --panel-bg: rgba(0, 0, 0, 0.85);
            --button-bg: linear-gradient(to right, #ff8a00, #da1b60);
            --button-hover-bg: rgba(255, 100, 100, 0.8);
            --stats-panel-bg: rgba(0, 0, 0, 0.7);
            --instructions-bg: rgba(0, 0, 0, 0.7);
            --lang-btn-bg: rgba(0, 0, 0, 0.7);
            --lang-btn-hover-bg: rgba(100, 100, 255, 0.8);
        }

        .light-theme {
            /* 浅色主题变量 */
            --bg-gradient-start: #a8edea;
            --bg-gradient-middle: #fed6e3;
            --bg-gradient-end: #d4fc79;
            --canvas-bg: radial-gradient(circle at center, #e0e0e0 0%, #a0a0a0 100%);
            --text-color: #333;
            --panel-bg: rgba(255, 255, 255, 0.85);
            --button-bg: linear-gradient(to right, #4a90e2, #50c878);
            --button-hover-bg: rgba(100, 200, 255, 0.8);
            --stats-panel-bg: rgba(255, 255, 255, 0.7);
            --instructions-bg: rgba(255, 255, 255, 0.7);
            --lang-btn-bg: rgba(255, 255, 255, 0.7);
            --lang-btn-hover-bg: rgba(150, 150, 255, 0.8);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        body {
            background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-middle), var(--bg-gradient-end));
            color: var(--text-color);
            min-height: 100dvh; /* 使用 dvh 适配移动端 */
            overflow: hidden;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 10px;
            transition: background-color 0.5s ease, color 0.5s ease; /* 主题切换过渡 */
        }
        .container {
            position: relative;
            width: 100%;
            /* max-width: 1400px; 移除或增大以适应全屏 */
            max-width: 100vw; /* 占满视口宽度 */
            height: 85vh; /* 增加高度 */
            margin: 20px auto;
            box-shadow: 0 0 30px rgba(0, 0, 0, 0.7);
            border-radius: 10px;
            overflow: hidden;
        }
        #fireworksCanvas {
            background: var(--canvas-bg);
            width: 100%;
            height: 100%;
            display: block;
        }
        .header {
            text-align: center;
            margin-bottom: 20px;
            z-index: 10;
            width: 100%;
        }
        h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
            text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
            background: var(--button-bg);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }
        .subtitle {
            font-size: 1.1rem;
            opacity: 0.8;
            max-width: 600px;
            margin: 0 auto;
        }
        .controls {
            position: absolute;
            top: 20px;
            right: 20px;
            z-index: 20;
            display: flex;
            gap: 10px;
        }
        .settings-btn, .stats-btn, .theme-btn {
            background: var(--lang-btn-bg);
            color: var(--text-color);
            border: none;
            padding: 12px 20px;
            border-radius: 30px;
            cursor: pointer;
            font-size: 1rem;
            font-weight: bold;
            transition: all 0.3s ease;
            backdrop-filter: blur(5px);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
        }
        .settings-btn:hover, .stats-btn:hover, .theme-btn:hover {
            background: var(--button-hover-bg);
            transform: translateY(-2px);
        }
        .settings-panel {
            position: absolute;
            top: 20px;
            right: 20px;
            width: 300px;
            background: var(--panel-bg);
            border-radius: 15px;
            padding: 20px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(10px);
            transform: translateX(0);
            transition: transform 0.5s ease;
            z-index: 30;
            border: 1px solid rgba(255, 255, 255, 0.1);
            color: var(--text-color);
        }
        .settings-panel.hidden {
            transform: translateX(350px);
        }
        .panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 1px solid rgba(255, 255, 255, 0.2);
        }
        .panel-title {
            font-size: 1.5rem;
            font-weight: bold;
        }
        .close-btn {
            background: none;
            border: none;
            color: var(--text-color);
            font-size: 1.5rem;
            cursor: pointer;
            width: 30px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 50%;
            transition: background 0.3s;
        }
        .close-btn:hover {
            background: rgba(255, 255, 255, 0.2);
        }
        .setting-group {
            margin-bottom: 20px;
        }
        .setting-label {
            display: block;
            margin-bottom: 8px;
            font-weight: 500;
        }
        .slider-container {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        input[type="range"] {
            flex: 1;
            height: 8px;
            -webkit-appearance: none;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 4px;
            outline: none;
        }
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background: #ff8a00;
            cursor: pointer;
            box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
        }
        .value-display {
            width: 40px;
            text-align: right;
            font-weight: bold;
        }
        .color-picker {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin-bottom: 15px;
        }
        .color-option {
            width: 30px;
            height: 30px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid rgba(255, 255, 255, 0.5);
            transition: transform 0.2s;
        }
        .color-option:hover {
            transform: scale(1.2);
        }
        .color-option.active {
            transform: scale(1.3);
            box-shadow: 0 0 10px white;
        }
        #customColorPicker {
            width: 100%;
            height: 40px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
        }
        .checkbox-container {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .checkbox-container input[type="checkbox"] {
            width: 20px;
            height: 20px;
            cursor: pointer;
        }
        /* 移除了 .buttons 类,因为按钮已移至主界面 */

        button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 8px;
            background: var(--button-bg);
            color: white;
            font-weight: bold;
            cursor: pointer;
            transition: all 0.3s;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
        }
        button:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4);
        }
        button:active {
            transform: translateY(1px);
        }
        /* 将使用说明移入设置面板 */
        .instructions {
            background: var(--instructions-bg);
            padding: 15px;
            border-radius: 10px;
            font-size: 0.9rem;
            backdrop-filter: blur(5px);
            color: var(--text-color);
            margin-top: 20px; /* 与上方设置项保持距离 */
        }
        .instructions h3 {
            margin-bottom: 10px;
            color: #ff8a00;
            font-size: 1.2rem; /* 缩小标题 */
        }
        .instructions ul {
            padding-left: 20px;
        }
        .instructions li {
            margin-bottom: 8px;
            font-size: 0.85rem; /* 缩小列表项字体 */
        }
        .language-switcher {
            position: absolute;
            top: 20px;
            left: 20px;
            z-index: 20;
        }
        .lang-btn {
            background: var(--lang-btn-bg);
            color: var(--text-color);
            border: none;
            padding: 10px 15px;
            border-radius: 30px;
            cursor: pointer;
            font-size: 0.9rem;
            transition: all 0.3s ease;
            backdrop-filter: blur(5px);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
        }
        .lang-btn:hover {
            background: var(--lang-btn-hover-bg);
        }
        .stats-panel {
            position: absolute;
            top: 20px;
            left: 20px;
            background: var(--stats-panel-bg);
            padding: 15px;
            border-radius: 10px;
            font-size: 0.9rem;
            backdrop-filter: blur(5px);
            z-index: 25;
            min-width: 150px;
            color: var(--text-color);
        }
        .stats-panel h3 {
            margin-bottom: 10px;
            color: #ff8a00;
        }
        .stats-item {
            margin-bottom: 5px;
        }
        .stats-panel.hidden {
            display: none; /* 使用 display: none 隐藏统计面板 */
        }
        
        /* 新增的主控制按钮区域 */
        .main-controls {
            display: flex;
            justify-content: center;
            gap: 20px;
            margin-top: 15px;
            z-index: 10;
            width: 100%;
        }
        .main-btn {
            padding: 15px 30px;
            font-size: 1.1rem;
        }

        /* 移动端优化 */
        @media (max-width: 768px) {
            .container {
                height: 70vh;
                max-width: 95vw;
            }
            h1 {
                font-size: 1.8rem;
            }
            .settings-panel {
                width: 250px;
                top: 10px;
                right: 10px;
            }
            /* 移除了原来的 .instructions 样式,因为已移入设置面板 */
            .language-switcher {
                top: 10px;
                left: 10px;
            }
            .settings-btn, .lang-btn, .stats-btn, .theme-btn {
                padding: 8px 12px; /* 减小按钮内边距 */
                font-size: 0.85rem; /* 减小字体 */
            }
            .panel-title {
                font-size: 1.2rem;
            }
            .main-controls {
                gap: 10px;
                margin-top: 10px;
            }
            .main-btn {
                padding: 12px 20px;
                font-size: 1rem;
            }
        }
        @media (max-width: 480px) {
            .container {
                height: 60vh;
            }
            h1 {
                font-size: 1.5rem;
            }
            .subtitle {
                font-size: 0.9rem;
            }
            .settings-panel {
                width: 220px;
                padding: 15px;
            }
            /* 移除了原来的 .instructions 样式,因为已移入设置面板 */
            .setting-label {
                font-size: 0.9rem;
            }
            .value-display {
                font-size: 0.8rem;
            }
            .controls {
                flex-direction: column;
            }
             .settings-btn, .lang-btn, .stats-btn, .theme-btn {
                padding: 6px 10px;
                font-size: 0.8rem;
            }
            .main-btn {
                padding: 10px 15px;
                font-size: 0.9rem;
            }
            /* 调整设置面板内说明的字体大小 */
            .instructions h3 {
                font-size: 1rem;
            }
            .instructions li {
                font-size: 0.75rem;
            }
        }
        /* iPad 特定样式 */
        @media screen and (min-width: 768px) and (max-width: 1024px) {
            .container {
                height: 75vh;
                max-width: 100vw;
            }
            h1 {
                font-size: 2rem;
            }
            .subtitle {
                font-size: 1rem;
            }
            .settings-panel {
                width: 280px;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1 id="pageTitle">✨ 火花秀模拟器 ✨</h1>
        <p class="subtitle" id="pageSubtitle">体验壮观的烟花表演,自定义设置。点击按钮切换设置面板。</p>
    </div>
    <div class="container">
        <canvas id="fireworksCanvas"></canvas>
        <div class="language-switcher">
            <button class="lang-btn" id="langToggle">English</button>
        </div>
        <div class="controls">
            <button class="theme-btn" id="themeToggle">🌓</button> <!-- 主题切换按钮 -->
            <button class="stats-btn" id="toggleStats">📊 统计</button>
            <button class="settings-btn" id="toggleSettings">⚙️ 设置</button>
        </div>
        <div class="settings-panel hidden" id="settingsPanel"> <!-- 默认隐藏 -->
            <div class="panel-header">
                <div class="panel-title" id="settingsTitle">烟花设置</div>
                <button class="close-btn" id="closeSettings">×</button>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="densityLabel">烟花密度</label>
                <div class="slider-container">
                    <input type="range" id="densitySlider" min="1" max="10" value="5">
                    <span class="value-display" id="densityValue">5</span>
                </div>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="particleLabel">粒子数量</label>
                <div class="slider-container">
                    <input type="range" id="particleSlider" min="50" max="500" value="200">
                    <span class="value-display" id="particleValue">200</span>
                </div>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="gravityLabel">重力强度</label>
                <div class="slider-container">
                    <input type="range" id="gravitySlider" min="0.1" max="2" step="0.1" value="0.5">
                    <span class="value-display" id="gravityValue">0.5</span>
                </div>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="sizeLabel">爆炸大小</label>
                <div class="slider-container">
                    <input type="range" id="sizeSlider" min="10" max="100" value="50">
                    <span class="value-display" id="sizeValue">50</span>
                </div>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="colorLabel">颜色方案</label>
                <div class="color-picker">
                    <div class="color-option active" style="background-color: #FF5252;" data-color="#FF5252"></div>
                    <div class="color-option" style="background-color: #448AFF;" data-color="#448AFF"></div>
                    <div class="color-option" style="background-color: #69F0AE;" data-color="#69F0AE"></div>
                    <div class="color-option" style="background-color: #FFD740;" data-color="#FFD740"></div>
                    <div class="color-option" style="background-color: #FF4081;" data-color="#FF4081"></div>
                    <div class="color-option" style="background-color: #7C4DFF;" data-color="#7C4DFF"></div>
                </div>
                <input type="color" id="customColorPicker" value="#FF5252">
            </div>
            <div class="setting-group">
                <label class="setting-label" id="typeLabel">烟花类型</label>
                <select id="typeSelect" style="width: 100%; padding: 10px; border-radius: 8px; background: rgba(255,255,255,0.1); color: var(--text-color); border: 1px solid rgba(255,255,255,0.2);">
                    <option value="classic" id="classicOption">经典爆炸</option>
                    <option value="star" id="starOption">星星形状</option>
                    <option value="ring" id="ringOption">圆环爆炸</option>
                    <option value="circular" id="circularOption">圆形图案</option>
                    <option value="heart" id="heartOption">心形图案</option>
                    <option value="spiral" id="spiralOption">螺旋图案</option>
                </select>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="autoLaunchLabel">自动发射</label>
                <div class="checkbox-container">
                    <input type="checkbox" id="autoLaunchToggle" checked>
                    <span id="autoLaunchText">开启</span>
                </div>
            </div>
            <div class="setting-group">
                <label class="setting-label" id="soundLabel">音效</label>
                <div class="checkbox-container">
                    <input type="checkbox" id="soundToggle" checked>
                    <span id="soundText">开启</span>
                </div>
            </div>
            <!-- 将使用说明移入设置面板 -->
            <div class="instructions">
                <h3 id="instructionsTitle">使用说明</h3>
                <ul>
                    <li id="step1">点击"发射烟花"创建新爆炸</li>
                    <li id="step2">使用滑块和颜色选择器调整设置</li>
                    <li id="step3">使用齿轮图标切换设置面板</li>
                    <li id="step4">尝试不同类型的烟花获得独特效果</li>
                    <li id="step5">点击重置清除所有烟花</li>
                    <li id="step6">点击暂停/继续按钮控制动画</li> <!-- 新增说明 -->
                </ul>
            </div>
            <!-- 按钮已移出,放到底部 -->
        </div>
        <div class="stats-panel hidden" id="statsPanel">
            <h3 id="statsTitle">统计信息</h3>
            <div class="stats-item" id="activeCount">活跃烟花: 0</div>
            <div class="stats-item" id="totalCount">总发射数: 0</div>
            <div class="stats-item" id="fpsCount">FPS: 0</div>
        </div>
        <!-- 原来的 .instructions 已移入 .settings-panel 内 -->
    </div>
    <!-- 新增的主控制按钮区域 -->
    <div class="main-controls">
        <button class="main-btn" id="launchBtnMain">发射烟花</button>
        <button class="main-btn" id="pauseBtn">⏸️ 暂停</button>
        <button class="main-btn" id="resetBtnMain">🔄 重置</button>
    </div>
    <audio id="explosionSound" preload="auto">
        <source src="https://assets.mixkit.co/sfx/preview/mixkit-fireworks-explosion-2845.mp3" type="audio/mpeg">
    </audio>
    <script>
        // 文本翻译对象
        const translations = {
            zh: {
                pageTitle: "✨ 火花秀模拟器 ✨",
                pageSubtitle: "体验壮观的烟花表演,自定义设置。点击按钮切换设置面板。",
                settingsTitle: "烟花设置",
                densityLabel: "烟花密度",
                particleLabel: "粒子数量",
                gravityLabel: "重力强度",
                sizeLabel: "爆炸大小",
                colorLabel: "颜色方案",
                typeLabel: "烟花类型",
                classicOption: "经典爆炸",
                starOption: "星星形状",
                ringOption: "圆环爆炸",
                circularOption: "圆形图案",
                heartOption: "心形图案",
                spiralOption: "螺旋图案",
                autoLaunchLabel: "自动发射",
                soundLabel: "音效",
                autoLaunchText: "开启",
                soundText: "开启",
                launchBtnText: "发射烟花",
                resetBtnText: "重置",
                statsTitle: "统计信息",
                activeCount: "活跃烟花: ",
                totalCount: "总发射数: ",
                fpsCount: "FPS: ",
                instructionsTitle: "使用说明",
                step1: "点击'发射烟花'创建新爆炸",
                step2: "使用滑块和颜色选择器调整设置",
                step3: "使用齿轮图标切换设置面板",
                step4: "尝试不同类型的烟花获得独特效果",
                step5: "点击重置清除所有烟花",
                step6: "点击暂停/继续按钮控制动画", // 新增翻译
                pauseBtnText: "⏸️ 暂停", // 新增翻译
                resumeBtnText: "▶️ 继续"  // 新增翻译
            },
            en: {
                pageTitle: "✨ Fireworks Simulator ✨",
                pageSubtitle: "Experience spectacular fireworks with customizable settings. Click the button to toggle the settings panel.",
                settingsTitle: "Fireworks Settings",
                densityLabel: "Firework Density",
                particleLabel: "Particle Count",
                gravityLabel: "Gravity Strength",
                sizeLabel: "Explosion Size",
                colorLabel: "Color Scheme",
                typeLabel: "Firework Type",
                classicOption: "Classic Burst",
                starOption: "Star Shape",
                ringOption: "Ring Explosion",
                circularOption: "Circular Pattern",
                heartOption: "Heart Shape",
                spiralOption: "Spiral Pattern",
                autoLaunchLabel: "Auto Launch",
                soundLabel: "Sound Effects",
                autoLaunchText: "On",
                soundText: "On",
                launchBtnText: "Launch Firework",
                resetBtnText: "Reset",
                statsTitle: "Statistics",
                activeCount: "Active: ",
                totalCount: "Total Launched: ",
                fpsCount: "FPS: ",
                instructionsTitle: "How to Use",
                step1: "Click 'Launch Firework' to create a new explosion",
                step2: "Adjust settings using sliders and color pickers",
                step3: "Toggle settings panel with the gear icon",
                step4: "Try different firework types for unique effects",
                step5: "Press reset to clear all fireworks",
                step6: "Click Pause/Resume button to control animation", // New translation
                pauseBtnText: "⏸️ Pause", // New translation
                resumeBtnText: "▶️ Resume"  // New translation
            }
        };
        // 当前语言
        let currentLang = 'zh';
        // 更新语言文本
        function updateLanguage() {
            const t = translations[currentLang];
            document.getElementById('pageTitle').textContent = t.pageTitle;
            document.getElementById('pageSubtitle').textContent = t.pageSubtitle;
            document.getElementById('settingsTitle').textContent = t.settingsTitle;
            document.getElementById('densityLabel').textContent = t.densityLabel;
            document.getElementById('particleLabel').textContent = t.particleLabel;
            document.getElementById('gravityLabel').textContent = t.gravityLabel;
            document.getElementById('sizeLabel').textContent = t.sizeLabel;
            document.getElementById('colorLabel').textContent = t.colorLabel;
            document.getElementById('typeLabel').textContent = t.typeLabel;
            document.getElementById('classicOption').textContent = t.classicOption;
            document.getElementById('starOption').textContent = t.starOption;
            document.getElementById('ringOption').textContent = t.ringOption;
            document.getElementById('circularOption').textContent = t.circularOption;
            document.getElementById('heartOption').textContent = t.heartOption;
            document.getElementById('spiralOption').textContent = t.spiralOption;
            document.getElementById('autoLaunchLabel').textContent = t.autoLaunchLabel;
            document.getElementById('soundLabel').textContent = t.soundLabel;
            document.getElementById('autoLaunchText').textContent = t.autoLaunchText;
            document.getElementById('soundText').textContent = t.soundText;
            // 主按钮文本由 JS 控制
            // document.getElementById('launchBtnMain').textContent = t.launchBtnText;
            // document.getElementById('resetBtnMain').textContent = t.resetBtnText;
            document.getElementById('statsTitle').textContent = t.statsTitle;
            document.getElementById('instructionsTitle').textContent = t.instructionsTitle;
            document.getElementById('step1').textContent = t.step1;
            document.getElementById('step2').textContent = t.step2;
            document.getElementById('step3').textContent = t.step3;
            document.getElementById('step4').textContent = t.step4;
            document.getElementById('step5').textContent = t.step5;
            document.getElementById('step6').textContent = t.step6; // 更新新增说明
            // 更新语言切换按钮文本
            document.getElementById('langToggle').textContent = currentLang === 'zh' ? 'English' : '中文';
            
            // 更新暂停按钮文本 (根据当前状态)
            const isPaused = document.getElementById('pauseBtn').textContent.includes('▶️') || document.getElementById('pauseBtn').textContent.includes('Resume');
            document.getElementById('pauseBtn').textContent = isPaused ? t.resumeBtnText : t.pauseBtnText;
        }
        
        // 随机颜色生成函数
        function getRandomColor() {
            const letters = '0123456789ABCDEF';
            let color = '#';
            for (let i = 0; i < 6; i++) {
                color += letters[Math.floor(Math.random() * 16)];
            }
            return color;
        }

        // Canvas setup
        const canvas = document.getElementById('fireworksCanvas');
        const ctx = canvas.getContext('2d');
        // Set canvas dimensions
        function resizeCanvas() {
            canvas.width = canvas.parentElement.clientWidth;
            canvas.height = canvas.parentElement.clientHeight;
        }
        window.addEventListener('resize', resizeCanvas);
        resizeCanvas();
        // Settings panel elements
        const settingsPanel = document.getElementById('settingsPanel');
        const toggleSettings = document.getElementById('toggleSettings');
        const closeSettings = document.getElementById('closeSettings');
        const toggleStats = document.getElementById('toggleStats');
        const statsPanel = document.getElementById('statsPanel');
        // Slider elements
        const densitySlider = document.getElementById('densitySlider');
        const particleSlider = document.getElementById('particleSlider');
        const gravitySlider = document.getElementById('gravitySlider');
        const sizeSlider = document.getElementById('sizeSlider');
        // Value displays
        const densityValue = document.getElementById('densityValue');
        const particleValue = document.getElementById('particleValue');
        const gravityValue = document.getElementById('gravityValue');
        const sizeValue = document.getElementById('sizeValue');
        // Color options
        const colorOptions = document.querySelectorAll('.color-option');
        const customColorPicker = document.getElementById('customColorPicker');
        // Buttons (主界面)
        const launchBtnMain = document.getElementById('launchBtnMain');
        const resetBtnMain = document.getElementById('resetBtnMain');
        const pauseBtn = document.getElementById('pauseBtn'); // 新增暂停按钮
        // Select element
        const typeSelect = document.getElementById('typeSelect');
        // Checkboxes
        const autoLaunchToggle = document.getElementById('autoLaunchToggle');
        const soundToggle = document.getElementById('soundToggle');
        // Language toggle
        const langToggle = document.getElementById('langToggle');
        // Theme toggle
        const themeToggle = document.getElementById('themeToggle'); // 新增主题切换按钮
        // Stats elements
        const activeCountEl = document.getElementById('activeCount');
        const totalCountEl = document.getElementById('totalCount');
        const fpsCountEl = document.getElementById('fpsCount');
        // Settings object
        let settings = {
            density: 5,
            particles: 200,
            gravity: 0.5,
            size: 50,
            color: '#FF5252',
            type: 'classic',
            autoLaunch: true,
            sound: true
        };
        // Statistics
        let stats = {
            totalLaunched: 0,
            fps: 0,
            lastFpsTime: Date.now(),
            frameCount: 0
        };
        // Animation control
        let animationId = null;
        let isPaused = false;

        // Firework class
        class Firework {
            constructor(x, y, targetX, targetY, color, type) {
                this.x = x;
                this.y = y;
                this.targetX = targetX;
                this.targetY = targetY;
                this.color = color;
                this.type = type;
                this.speed = 5;
                this.angle = Math.atan2(targetY - y, targetX - x);
                this.velocity = {
                    x: Math.cos(this.angle) * this.speed,
                    y: Math.sin(this.angle) * this.speed
                };
                this.trail = [];
                this.trailLength = 10;
                this.exploded = false;
                this.particles = [];
                this.particleCount = settings.particles;
                this.size = settings.size;
                // Create trail points
                for (let i = 0; i < this.trailLength; i++) {
                    this.trail.push({
                        x: this.x,
                        y: this.y,
                        life: i / this.trailLength
                    });
                }
            }
            update() {
                if (!this.exploded) {
                    // Move towards target
                    this.x += this.velocity.x;
                    this.y += this.velocity.y;
                    // Add to trail
                    this.trail.push({x: this.x, y: this.y, life: 1});
                    if (this.trail.length > this.trailLength) {
                        this.trail.shift();
                    }
                    // Check if reached target
                    const dx = this.targetX - this.x;
                    const dy = this.targetY - this.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    if (distance < 5) {
                        this.explode();
                    }
                } else {
                    // Update particles
                    for (let i = this.particles.length - 1; i >= 0; i--) {
                        this.particles[i].update();
                        if (this.particles[i].life <= 0) {
                            this.particles.splice(i, 1);
                        }
                    }
                }
            }
            explode() {
                this.exploded = true;
                // Play sound if enabled
                if (settings.sound) {
                    const sound = document.getElementById('explosionSound');
                    sound.currentTime = 0;
                    sound.play().catch(e => console.log("Audio play failed:", e));
                }
                // Create explosion particles based on type
                switch (this.type) {
                    case 'classic':
                        this.createClassicExplosion();
                        break;
                    case 'star':
                        this.createStarExplosion();
                        break;
                    case 'ring':
                        this.createRingExplosion();
                        break;
                    case 'circular':
                        this.createCircularExplosion();
                        break;
                    case 'heart':
                        this.createHeartExplosion();
                        break;
                    case 'spiral':
                        this.createSpiralExplosion();
                        break;
                }
            }
            createClassicExplosion() {
                for (let i = 0; i < this.particleCount; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = Math.random() * 5 + 2;
                    const size = Math.random() * 3 + 1;
                    this.particles.push(new Particle(
                        this.x, 
                        this.y, 
                        Math.cos(angle) * speed, 
                        Math.sin(angle) * speed, 
                        size, 
                        this.color
                    ));
                }
            }
            createStarExplosion() {
                for (let i = 0; i < this.particleCount; i++) {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = Math.random() * 6 + 3;
                    const size = Math.random() * 4 + 2;
                    // Create star shape by varying the direction
                    const direction = Math.random() > 0.5 ? 1 : -1;
                    const offsetX = Math.cos(angle) * direction * 10;
                    const offsetY = Math.sin(angle) * direction * 10;
                    this.particles.push(new Particle(
                        this.x + offsetX, 
                        this.y + offsetY, 
                        Math.cos(angle) * speed, 
                        Math.sin(angle) * speed, 
                        size, 
                        this.color
                    ));
                }
            }
            createRingExplosion() {
                const ringCount = 5;
                const ringRadius = 20;
                for (let r = 0; r < ringCount; r++) {
                    for (let i = 0; i < this.particleCount / ringCount; i++) {
                        const angle = (i / (this.particleCount / ringCount)) * Math.PI * 2;
                        const size = Math.random() * 3 + 1;
                        const radius = ringRadius * (r + 1);
                        this.particles.push(new Particle(
                            this.x + Math.cos(angle) * radius, 
                            this.y + Math.sin(angle) * radius, 
                            Math.cos(angle) * 3, 
                            Math.sin(angle) * 3, 
                            size, 
                            this.color
                        ));
                    }
                }
            }
            createCircularExplosion() {
                const segments = 12;
                const angleStep = (Math.PI * 2) / segments;
                for (let s = 0; s < segments; s++) {
                    for (let i = 0; i < this.particleCount / segments; i++) {
                        const angle = s * angleStep + (Math.random() * angleStep * 0.5);
                        const speed = Math.random() * 5 + 2;
                        const size = Math.random() * 3 + 1;
                        this.particles.push(new Particle(
                            this.x, 
                            this.y, 
                            Math.cos(angle) * speed, 
                            Math.sin(angle) * speed, 
                            size, 
                            this.color
                        ));
                    }
                }
            }
            createHeartExplosion() {
                for (let i = 0; i < this.particleCount; i++) {
                    // Heart parametric equations
                    const t = Math.random() * Math.PI * 2;
                    const scale = 20;
                    const x = 16 * Math.pow(Math.sin(t), 3);
                    const y = -(13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t));
                    const speed = Math.random() * 3 + 1;
                    const size = Math.random() * 3 + 1;
                    this.particles.push(new Particle(
                        this.x, 
                        this.y, 
                        x * speed / scale, 
                        y * speed / scale, 
                        size, 
                        this.color
                    ));
                }
            }
            createSpiralExplosion() {
                for (let i = 0; i < this.particleCount; i++) {
                    const angle = i * 0.1;
                    const radius = i * 0.5;
                    const speed = Math.random() * 2 + 1;
                    const size = Math.random() * 3 + 1;
                    this.particles.push(new Particle(
                        this.x + Math.cos(angle) * radius, 
                        this.y + Math.sin(angle) * radius, 
                        Math.cos(angle) * speed, 
                        Math.sin(angle) * speed, 
                        size, 
                        this.color
                    ));
                }
            }
            draw() {
                if (!this.exploded) {
                    // Draw trail
                    for (let i = 0; i < this.trail.length; i++) {
                        const point = this.trail[i];
                        const alpha = point.life;
                        const size = 3 * alpha;
                        ctx.beginPath();
                        ctx.arc(point.x, point.y, size, 0, Math.PI * 2);
                        ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.5})`;
                        ctx.fill();
                    }
                    // Draw firework head
                    ctx.beginPath();
                    ctx.arc(this.x, this.y, 5, 0, Math.PI * 2);
                    ctx.fillStyle = this.color;
                    ctx.fill();
                } else {
                    // Draw particles
                    for (const particle of this.particles) {
                        particle.draw();
                    }
                }
            }
        }
        // Particle class
        class Particle {
            constructor(x, y, vx, vy, size, color) {
                this.x = x;
                this.y = y;
                this.vx = vx;
                this.vy = vy;
                this.size = size;
                this.color = color;
                this.life = 1;
                this.gravity = settings.gravity;
                this.friction = 0.98;
            }
            update() {
                this.x += this.vx;
                this.y += this.vy;
                this.vy += this.gravity;
                this.vx *= this.friction;
                this.vy *= this.friction;
                this.life -= 0.01;
            }
            draw() {
                const alpha = this.life;
                ctx.beginPath();
                ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
                // Ensure color is in rgb format for alpha replacement
                let colorToUse = this.color;
                if (colorToUse.startsWith('#')) {
                    const r = parseInt(colorToUse.substr(1, 2), 16);
                    const g = parseInt(colorToUse.substr(3, 2), 16);
                    const b = parseInt(colorToUse.substr(5, 2), 16);
                    colorToUse = `rgb(${r},${g},${b})`;
                }
                ctx.fillStyle = colorToUse.replace(')', `, ${alpha})`).replace('rgb', 'rgba');
                ctx.fill();
            }
        }
        // Array to hold all fireworks
        let fireworks = [];
        let lastTime = 0;
        
        // Initialize settings from UI
        function initSettings() {
            densityValue.textContent = settings.density;
            particleValue.textContent = settings.particles;
            gravityValue.textContent = settings.gravity.toFixed(1);
            sizeValue.textContent = settings.size;
            densitySlider.value = settings.density;
            particleSlider.value = settings.particles;
            gravitySlider.value = settings.gravity;
            sizeSlider.value = settings.size;
            autoLaunchToggle.checked = settings.autoLaunch;
            soundToggle.checked = settings.sound;
            // Set active color
            colorOptions.forEach(option => {
                option.classList.remove('active');
                if (option.dataset.color === settings.color) {
                    option.classList.add('active');
                }
            });
            customColorPicker.value = settings.color;
            typeSelect.value = settings.type;
        }
        
        // Save settings to localStorage
        function saveSettings() {
            localStorage.setItem('fireworksSettings', JSON.stringify(settings));
        }
        // Load settings from localStorage
        function loadSettings() {
            const saved = localStorage.getItem('fireworksSettings');
            if (saved) {
                const parsed = JSON.parse(saved);
                Object.assign(settings, parsed);
                initSettings();
            }
        }
        // Launch a firework
        function launchFirework() {
            if (fireworks.length >= 20) return; // Limit max fireworks
            const x = Math.random() * canvas.width;
            const y = canvas.height;
            const targetX = Math.random() * canvas.width;
            const targetY = Math.random() * (canvas.height * 0.6);
            // 使用随机颜色
            const randomColor = getRandomColor();
            fireworks.push(new Firework(x, y, targetX, targetY, randomColor, settings.type));
            stats.totalLaunched++;
        }
        // Reset all fireworks
        function resetFireworks() {
            fireworks = [];
        }
        // Update statistics display
        function updateStats() {
            activeCountEl.textContent = translations[currentLang].activeCount + fireworks.length;
            totalCountEl.textContent = translations[currentLang].totalCount + stats.totalLaunched;
            fpsCountEl.textContent = translations[currentLang].fpsCount + stats.fps;
        }
        // Animation loop
        function animate(timestamp) {
            if (isPaused) {
                 animationId = requestAnimationFrame(animate);
                 return;
            }
            // Calculate time delta
            const deltaTime = timestamp - lastTime;
            lastTime = timestamp;
            // FPS calculation
            stats.frameCount++;
            if (Date.now() - stats.lastFpsTime >= 1000) {
                stats.fps = stats.frameCount;
                stats.frameCount = 0;
                stats.lastFpsTime = Date.now();
                updateStats();
            }
            // Clear canvas with a semi-transparent overlay for trail effect
            ctx.fillStyle = 'rgba(10, 10, 30, 0.2)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            // Update and draw fireworks
            for (let i = fireworks.length - 1; i >= 0; i--) {
                fireworks[i].update();
                fireworks[i].draw();
                // Remove exploded fireworks when all particles are gone
                if (fireworks[i].exploded && fireworks[i].particles.length === 0) {
                    fireworks.splice(i, 1);
                }
            }
            // Randomly launch fireworks based on density setting
            if (settings.autoLaunch && Math.random() < settings.density / 100) {
                launchFirework();
            }
            animationId = requestAnimationFrame(animate);
        }
        
        // 切换暂停/继续状态
        function togglePause() {
             isPaused = !isPaused;
             const t = translations[currentLang];
             if (isPaused) {
                 pauseBtn.textContent = t.resumeBtnText;
                 if (animationId) {
                     cancelAnimationFrame(animationId);
                     animationId = null;
                 }
             } else {
                 pauseBtn.textContent = t.pauseBtnText;
                 lastTime = performance.now(); // 重置时间以避免跳帧
                 animationId = requestAnimationFrame(animate);
             }
        }

        // Event listeners for settings
        densitySlider.addEventListener('input', () => {
            densityValue.textContent = densitySlider.value;
            settings.density = parseInt(densitySlider.value);
            saveSettings();
        });
        particleSlider.addEventListener('input', () => {
            particleValue.textContent = particleSlider.value;
            settings.particles = parseInt(particleSlider.value);
            saveSettings();
        });
        gravitySlider.addEventListener('input', () => {
            gravityValue.textContent = gravitySlider.value;
            settings.gravity = parseFloat(gravitySlider.value);
            saveSettings();
        });
        sizeSlider.addEventListener('input', () => {
            sizeValue.textContent = sizeSlider.value;
            settings.size = parseInt(sizeSlider.value);
            saveSettings();
        });
        colorOptions.forEach(option => {
            option.addEventListener('click', () => {
                colorOptions.forEach(opt => opt.classList.remove('active'));
                option.classList.add('active');
                settings.color = option.dataset.color;
                customColorPicker.value = settings.color;
                saveSettings();
            });
        });
        customColorPicker.addEventListener('input', function() {
            settings.color = this.value;
            colorOptions.forEach(opt => opt.classList.remove('active'));
            saveSettings();
        });
        typeSelect.addEventListener('change', () => {
            settings.type = typeSelect.value;
            saveSettings();
        });
        autoLaunchToggle.addEventListener('change', () => {
            settings.autoLaunch = autoLaunchToggle.checked;
            saveSettings();
        });
        soundToggle.addEventListener('change', () => {
            settings.sound = soundToggle.checked;
            saveSettings();
        });
        // Button event listeners (主界面)
        toggleSettings.addEventListener('click', () => {
            settingsPanel.classList.toggle('hidden');
        });
        closeSettings.addEventListener('click', () => {
            settingsPanel.classList.add('hidden');
        });
        toggleStats.addEventListener('click', () => {
            statsPanel.classList.toggle('hidden');
        });
        launchBtnMain.addEventListener('click', launchFirework);
        resetBtnMain.addEventListener('click', resetFireworks);
        pauseBtn.addEventListener('click', togglePause); // 新增暂停按钮事件
        // 语言切换
        langToggle.addEventListener('click', () => {
            currentLang = currentLang === 'zh' ? 'en' : 'zh';
            updateLanguage();
            updateStats();
        });
        
        // 主题切换
        themeToggle.addEventListener('click', () => {
            document.body.classList.toggle('light-theme');
            // 可以选择在这里保存主题偏好到 localStorage
            // localStorage.setItem('theme', document.body.classList.contains('light-theme') ? 'light' : 'dark');
        });

        // 初始化和开始动画
        loadSettings();
        updateLanguage();
        // 启动动画循环
        animationId = requestAnimationFrame(animate);
        // Launch initial fireworks
        setTimeout(() => {
            for (let i = 0; i < 5; i++) {
                setTimeout(() => launchFirework(), i * 500);
            }
        }, 1000);
        // Service Worker registration for PWA
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('sw.js')
                    .then(registration => console.log('SW registered:', registration))
                    .catch(error => console.log('SW registration failed:', error));
            });
        }
    </script>
</body>
</html>
        
编辑器加载中
预览
控制台