<!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>
index.html
index.html