• 用HTML和CSS打造跨年烟花秀视觉盛宴


    目录

    一、程序代码

    二、代码原理

    三、运行效果


    一、程序代码

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>跨年烟花秀title>
    6. <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    7. <meta name="mobile-web-app-capable" content="yes">
    8. <meta name="apple-mobile-web-app-capable" content="yes">
    9. <meta name="theme-color" content="#000000">
    10. <link rel="shortcut icon" type="image/png"
    11. href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
    12. <link rel="icon" type="image/png"
    13. href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
    14. <link rel="apple-touch-icon-precomposed"
    15. href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
    16. <meta name="msapplication-TileColor" content="#000000">
    17. <meta name="msapplication-TileImage"
    18. content="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
    19. <link href="https://fonts.googleapis.com/css?family=Russo+One" rel="stylesheet">
    20. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
    21. <link rel="stylesheet" href="./style.css">
    22. <style>
    23. * {
    24. position: relative;
    25. box-sizing: border-box;
    26. }
    27. html,
    28. body {
    29. height: 100%;
    30. }
    31. html {
    32. background-color: #000;
    33. }
    34. body {
    35. overflow: hidden;
    36. color: rgba(255, 255, 255, 0.5);
    37. font-family: "Russo One", arial, sans-serif;
    38. line-height: 1.25;
    39. letter-spacing: 0.06em;
    40. }
    41. .hide {
    42. opacity: 0;
    43. visibility: hidden;
    44. }
    45. .remove {
    46. display: none;
    47. }
    48. .blur {
    49. filter: blur(12px);
    50. }
    51. .container {
    52. height: 100%;
    53. display: flex;
    54. justify-content: center;
    55. align-items: center;
    56. }
    57. #loading-init {
    58. width: 100%;
    59. align-self: center;
    60. text-align: center;
    61. font-size: 2em;
    62. }
    63. #stage-container {
    64. overflow: hidden;
    65. box-sizing: initial;
    66. border: 1px solid #222;
    67. margin: -1px;
    68. }
    69. #canvas-container {
    70. width: 100%;
    71. height: 100%;
    72. transition: filter 0.3s;
    73. }
    74. #canvas-container canvas {
    75. position: absolute;
    76. mix-blend-mode: lighten;
    77. }
    78. #controls {
    79. position: absolute;
    80. top: 0;
    81. width: 100%;
    82. padding-bottom: 50px;
    83. display: flex;
    84. justify-content: space-between;
    85. transition: opacity 0.3s, visibility 0.3s;
    86. }
    87. @media (min-width: 800px) {
    88. #controls {
    89. visibility: visible;
    90. }
    91. #controls.hide:hover {
    92. opacity: 1;
    93. }
    94. }
    95. #menu {
    96. display: flex;
    97. flex-direction: column;
    98. justify-content: center;
    99. align-items: center;
    100. position: absolute;
    101. top: 0;
    102. bottom: 0;
    103. width: 100%;
    104. background-color: rgba(0, 0, 0, 0.42);
    105. transition: opacity 0.3s, visibility 0.3s;
    106. }
    107. #menu__header {
    108. padding: 20px 0 44px;
    109. font-size: 2em;
    110. text-transform: uppercase;
    111. }
    112. #menu form {
    113. width: 240px;
    114. padding: 0 20px;
    115. overflow: auto;
    116. }
    117. #menu .form-option {
    118. margin: 20px 0;
    119. }
    120. #menu .form-option label {
    121. text-transform: uppercase;
    122. }
    123. #menu .form-option--select label {
    124. display: block;
    125. margin-bottom: 6px;
    126. }
    127. #menu .form-option--select select {
    128. display: block;
    129. width: 100%;
    130. height: 30px;
    131. font-size: 1rem;
    132. font-family: "Russo One", arial, sans-serif;
    133. color: rgba(255, 255, 255, 0.5);
    134. letter-spacing: 0.06em;
    135. background-color: transparent;
    136. border: 1px solid rgba(255, 255, 255, 0.5);
    137. }
    138. #menu .form-option--select select option {
    139. background-color: black;
    140. }
    141. #menu .form-option--checkbox label {
    142. display: flex;
    143. align-items: center;
    144. transition: opacity 0.3s;
    145. -webkit-user-select: none;
    146. -moz-user-select: none;
    147. -ms-user-select: none;
    148. user-select: none;
    149. }
    150. #menu .form-option--checkbox input {
    151. display: block;
    152. width: 20px;
    153. height: 20px;
    154. margin-right: 8px;
    155. opacity: 0.5;
    156. }
    157. @media (max-width: 800px) {
    158. #menu .form-option select,
    159. #menu .form-option input {
    160. outline: none;
    161. }
    162. }
    163. #close-menu-btn {
    164. position: absolute;
    165. top: 0;
    166. right: 0;
    167. }
    168. .btn {
    169. opacity: 0.16;
    170. width: 44px;
    171. height: 44px;
    172. display: flex;
    173. -webkit-user-select: none;
    174. -moz-user-select: none;
    175. -ms-user-select: none;
    176. user-select: none;
    177. cursor: default;
    178. transition: opacity 0.3s;
    179. }
    180. .btn--bright {
    181. opacity: 0.5;
    182. }
    183. @media (min-width: 800px) {
    184. .btn:hover {
    185. opacity: 0.32;
    186. }
    187. .btn--bright:hover {
    188. opacity: 0.75;
    189. }
    190. }
    191. .btn svg {
    192. display: block;
    193. margin: auto;
    194. }
    195. style>
    196. head>
    197. <body>
    198. <div style="height: 0; width: 0; position: absolute; visibility: hidden;">
    199. <svg xmlns="http://www.w3.org/2000/svg">
    200. <symbol id="icon-play" viewBox="0 0 24 24">
    201. <path d="M8 5v14l11-7z" />
    202. symbol>
    203. <symbol id="icon-pause" viewBox="0 0 24 24">
    204. <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
    205. symbol>
    206. <symbol id="icon-close" viewBox="0 0 24 24">
    207. <path
    208. d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
    209. symbol>
    210. <symbol id="icon-settings" viewBox="0 0 24 24">
    211. <path
    212. d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" />
    213. symbol>
    214. <symbol id="icon-shutter-fast" viewBox="0 0 24 24">
    215. <path
    216. d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" />
    217. symbol>
    218. <symbol id="icon-shutter-slow" viewBox="0 0 24 24">
    219. <path
    220. d="M1 5h2v14H1zm4 0h2v14H5zm17 0H10c-.55 0-1 .45-1 1v12c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1zM11 17l2.5-3.15L15.29 16l2.5-3.22L21 17H11z" />
    221. symbol>
    222. svg>
    223. div>
    224. <div class="container">
    225. <div id="loading-init">惊喜即将来临!div>
    226. <div id="stage-container" class="remove">
    227. <div id="canvas-container">
    228. <canvas id="trails-canvas">canvas>
    229. <canvas id="main-canvas">canvas>
    230. div>
    231. <div id="controls">
    232. <div id="pause-btn" class="btn">
    233. <svg fill="white" width="24" height="24">
    234. <use href="#icon-pause">use>
    235. svg>
    236. div>
    237. <div id="shutter-btn" class="btn">
    238. <svg fill="white" width="24" height="24">
    239. <use href="#icon-shutter-slow">use>
    240. svg>
    241. div>
    242. <div id="settings-btn" class="btn">
    243. <svg fill="white" width="24" height="24">
    244. <use href="#icon-settings">use>
    245. svg>
    246. div>
    247. div>
    248. <div id="menu" class="hide">
    249. <div id="close-menu-btn" class="btn btn--bright">
    250. <svg fill="white" width="24" height="24">
    251. <use href="#icon-close">use>
    252. svg>
    253. div>
    254. <div id="menu__header">Settingsdiv>
    255. <form>
    256. <div class="form-option form-option--select">
    257. <label>Shell Typelabel>
    258. <select id="shell-type">select>
    259. div>
    260. <div class="form-option form-option--select">
    261. <label>Shell Sizelabel>
    262. <select id="shell-size">select>
    263. div>
    264. <div class="form-option form-option--checkbox">
    265. <label id="auto-launch-label"><input id="auto-launch" type="checkbox" /><span>Auto
    266. Firespan>label>
    267. div>
    268. <div class="form-option form-option--checkbox">
    269. <label id="finale-mode-label"><input id="finale-mode" type="checkbox" /><span>Finale
    270. Modespan>label>
    271. div>
    272. <div class="form-option form-option--checkbox">
    273. <label id="hide-controls-label"><input id="hide-controls" type="checkbox" /><span>Hide
    274. Controlsspan>label>
    275. div>
    276. form>
    277. div>
    278. div>
    279. div>
    280. <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/fscreen%401.0.1.js'>script>
    281. <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/Stage%400.1.4.js'>script>
    282. <script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/MyMath.js'>script>
    283. <script>
    284. 'use strict';
    285. console.clear();
    286. const IS_MOBILE = window.innerWidth <= 640;
    287. const IS_DESKTOP = window.innerWidth > 800;
    288. const IS_HEADER = IS_DESKTOP && window.innerHeight < 300;
    289. // 8K - can restrict this if needed
    290. const MAX_WIDTH = 7680;
    291. const MAX_HEIGHT = 4320;
    292. const GRAVITY = 0.9; // Acceleration in px/s
    293. let simSpeed = 1;
    294. const COLOR = {
    295. Red: '#ff0043',
    296. Green: '#14fc56',
    297. Blue: '#1e7fff',
    298. Purple: '#e60aff',
    299. Gold: '#ffae00',
    300. White: '#ffffff'
    301. };
    302. // Special invisible color (not rendered, and therefore not in COLOR map)
    303. const INVISIBLE = '_INVISIBLE_';
    304. // Interactive state management
    305. const store = {
    306. _listeners: new Set(),
    307. _dispatch() {
    308. this._listeners.forEach(listener => listener(this.state))
    309. },
    310. state: {
    311. paused: false,
    312. longExposure: false,
    313. menuOpen: false,
    314. config: {
    315. shell: 'Random',
    316. size: IS_DESKTOP && !IS_HEADER ? '3' : '1',
    317. autoLaunch: true,
    318. finale: false,
    319. hideControls: IS_HEADER
    320. }
    321. },
    322. setState(nextState) {
    323. this.state = Object.assign({}, this.state, nextState);
    324. this._dispatch();
    325. this.persist();
    326. },
    327. subscribe(listener) {
    328. this._listeners.add(listener);
    329. return () => this._listeners.remove(listener);
    330. },
    331. // Load / persist select state to localStorage
    332. load() {
    333. if (localStorage.getItem('schemaVersion') === '1') {
    334. this.state.config.size = JSON.parse(localStorage.getItem('configSize'));
    335. this.state.config.hideControls = JSON.parse(localStorage.getItem('hideControls'));
    336. }
    337. },
    338. persist() {
    339. localStorage.setItem('schemaVersion', '1');
    340. localStorage.setItem('configSize', JSON.stringify(this.state.config.size));
    341. localStorage.setItem('hideControls', JSON.stringify(this.state.config.hideControls));
    342. }
    343. };
    344. if (!IS_HEADER) {
    345. store.load();
    346. }
    347. // Actions
    348. // ---------
    349. function togglePause(toggle) {
    350. if (typeof toggle === 'boolean') {
    351. store.setState({ paused: toggle });
    352. } else {
    353. store.setState({ paused: !store.state.paused });
    354. }
    355. }
    356. function toggleLongExposure(toggle) {
    357. if (typeof toggle === 'boolean') {
    358. store.setState({ longExposure: toggle });
    359. } else {
    360. store.setState({ longExposure: !store.state.longExposure });
    361. }
    362. }
    363. function toggleMenu(toggle) {
    364. if (typeof toggle === 'boolean') {
    365. store.setState({ menuOpen: toggle });
    366. } else {
    367. store.setState({ menuOpen: !store.state.menuOpen });
    368. }
    369. }
    370. function updateConfig(nextConfig) {
    371. nextConfig = nextConfig || getConfigFromDOM();
    372. store.setState({
    373. config: Object.assign({}, store.state.config, nextConfig)
    374. });
    375. }
    376. // Selectors
    377. // -----------
    378. const canInteract = () => !store.state.paused && !store.state.menuOpen;
    379. const shellNameSelector = () => store.state.config.shell;
    380. // Converts shell size to number.
    381. const shellSizeSelector = () => +store.state.config.size;
    382. const finaleSelector = () => store.state.config.finale;
    383. // Render app UI / keep in sync with state
    384. const appNodes = {
    385. stageContainer: '#stage-container',
    386. canvasContainer: '#canvas-container',
    387. controls: '#controls',
    388. menu: '#menu',
    389. pauseBtn: '#pause-btn',
    390. pauseBtnSVG: '#pause-btn use',
    391. shutterBtn: '#shutter-btn',
    392. shutterBtnSVG: '#shutter-btn use',
    393. shellType: '#shell-type',
    394. shellSize: '#shell-size',
    395. autoLaunch: '#auto-launch',
    396. autoLaunchLabel: '#auto-launch-label',
    397. finaleMode: '#finale-mode',
    398. finaleModeLabel: '#finale-mode-label',
    399. hideControls: '#hide-controls',
    400. hideControlsLabel: '#hide-controls-label'
    401. };
    402. // Convert appNodes selectors to dom nodes
    403. Object.keys(appNodes).forEach(key => {
    404. appNodes[key] = document.querySelector(appNodes[key]);
    405. });
    406. // Remove loading state
    407. document.getElementById('loading-init').remove();
    408. appNodes.stageContainer.classList.remove('remove');
    409. // First render is called in init()
    410. function renderApp(state) {
    411. appNodes.pauseBtnSVG.setAttribute('href', `#icon-${state.paused ? 'play' : 'pause'}`);
    412. appNodes.shutterBtnSVG.setAttribute('href', `#icon-shutter-${state.longExposure ? 'fast' : 'slow'}`);
    413. appNodes.controls.classList.toggle('hide', state.menuOpen || state.config.hideControls);
    414. appNodes.canvasContainer.classList.toggle('blur', state.menuOpen);
    415. appNodes.menu.classList.toggle('hide', !state.menuOpen);
    416. appNodes.finaleModeLabel.style.opacity = state.config.autoLaunch ? 1 : 0.32;
    417. appNodes.shellType.value = state.config.shell;
    418. appNodes.shellSize.value = state.config.size;
    419. appNodes.autoLaunch.checked = state.config.autoLaunch;
    420. appNodes.finaleMode.checked = state.config.finale;
    421. appNodes.hideControls.checked = state.config.hideControls;
    422. }
    423. store.subscribe(renderApp);
    424. function getConfigFromDOM() {
    425. return {
    426. shell: appNodes.shellType.value,
    427. size: appNodes.shellSize.value,
    428. autoLaunch: appNodes.autoLaunch.checked,
    429. finale: appNodes.finaleMode.checked,
    430. hideControls: appNodes.hideControls.checked
    431. };
    432. };
    433. const updateConfigNoEvent = () => updateConfig();
    434. appNodes.shellType.addEventListener('input', updateConfigNoEvent);
    435. appNodes.shellSize.addEventListener('input', updateConfigNoEvent);
    436. appNodes.autoLaunchLabel.addEventListener('click', () => setTimeout(updateConfig, 0));
    437. appNodes.finaleModeLabel.addEventListener('click', () => setTimeout(updateConfig, 0));
    438. appNodes.hideControlsLabel.addEventListener('click', () => setTimeout(updateConfig, 0));
    439. // Constant derivations
    440. const COLOR_NAMES = Object.keys(COLOR);
    441. const COLOR_CODES = COLOR_NAMES.map(colorName => COLOR[colorName]);
    442. // Invisible stars need an indentifier, even through they won't be rendered - physics still apply.
    443. const COLOR_CODES_W_INVIS = [...COLOR_CODES, INVISIBLE];
    444. // Tuples is a map keys by color codes (hex) with values of { r, g, b } tuples (still just objects).
    445. const COLOR_TUPLES = {};
    446. COLOR_CODES.forEach(hex => {
    447. COLOR_TUPLES[hex] = {
    448. r: parseInt(hex.substr(1, 2), 16),
    449. g: parseInt(hex.substr(3, 2), 16),
    450. b: parseInt(hex.substr(5, 2), 16),
    451. };
    452. });
    453. // Get a random color.
    454. function randomColorSimple() {
    455. return COLOR_CODES[Math.random() * COLOR_CODES.length | 0];
    456. }
    457. // Get a random color, with some customization options available.
    458. let lastColor;
    459. function randomColor(options) {
    460. const notSame = options && options.notSame;
    461. const notColor = options && options.notColor;
    462. const limitWhite = options && options.limitWhite;
    463. let color = randomColorSimple();
    464. // limit the amount of white chosen randomly
    465. if (limitWhite && color === COLOR.White && Math.random() < 0.6) {
    466. color = randomColorSimple();
    467. }
    468. if (notSame) {
    469. while (color === lastColor) {
    470. color = randomColorSimple();
    471. }
    472. }
    473. else if (notColor) {
    474. while (color === notColor) {
    475. color = randomColorSimple();
    476. }
    477. }
    478. lastColor = color;
    479. return color;
    480. }
    481. function whiteOrGold() {
    482. return Math.random() < 0.5 ? COLOR.Gold : COLOR.White;
    483. }
    484. const PI_2 = Math.PI * 2;
    485. const PI_HALF = Math.PI * 0.5;
    486. const trailsStage = new Stage('trails-canvas');
    487. const mainStage = new Stage('main-canvas');
    488. const stages = [
    489. trailsStage,
    490. mainStage
    491. ];
    492. // Fill trails canvas with black to start.
    493. trailsStage.ctx.fillStyle = '#000';
    494. trailsStage.ctx.fillRect(0, 0, trailsStage.width, trailsStage.height);
    495. // Fullscreen helpers, using Fscreen for prefixes
    496. function requestFullscreen() {
    497. if (fullscreenEnabled() && !isFullscreen()) {
    498. fscreen.requestFullscreen(document.documentElement);
    499. }
    500. }
    501. function fullscreenEnabled() {
    502. return fscreen.fullscreenEnabled;
    503. }
    504. function isFullscreen() {
    505. return !!fscreen.fullscreenElement;
    506. }
    507. // Shell helpers
    508. function makePistilColor(shellColor) {
    509. return (shellColor === COLOR.White || shellColor === COLOR.Gold) ? randomColor({ notColor: shellColor }) : whiteOrGold();
    510. }
    511. // Unique shell types
    512. const crysanthemumShell = (size = 1) => {
    513. const glitter = Math.random() < 0.25;
    514. const singleColor = Math.random() < 0.68;
    515. const color = singleColor ? randomColor({ limitWhite: true }) : [randomColor(), randomColor({ notSame: true })];
    516. const pistil = singleColor && Math.random() < 0.42;
    517. const pistilColor = makePistilColor(color);
    518. const streamers = !pistil && color !== COLOR.White && Math.random() < 0.42;
    519. return {
    520. size: 300 + size * 100,
    521. starLife: 900 + size * 200,
    522. starDensity: glitter ? 1.1 : 1.5,
    523. color,
    524. glitter: glitter ? 'light' : '',
    525. glitterColor: whiteOrGold(),
    526. pistil,
    527. pistilColor,
    528. streamers
    529. };
    530. };
    531. const palmShell = (size = 1) => ({
    532. size: 250 + size * 75,
    533. starDensity: 0.6,
    534. starLife: 1800 + size * 200,
    535. glitter: 'heavy'
    536. });
    537. const ringShell = (size = 1) => {
    538. const color = randomColor();
    539. const pistil = Math.random() < 0.75;
    540. return {
    541. ring: true,
    542. color,
    543. size: 300 + size * 100,
    544. starLife: 900 + size * 200,
    545. starCount: 2.2 * PI_2 * (size + 1),
    546. pistil,
    547. pistilColor: makePistilColor(color),
    548. glitter: !pistil ? 'light' : '',
    549. glitterColor: color === COLOR.Gold ? COLOR.Gold : COLOR.White
    550. };
    551. };
    552. const crossetteShell = (size = 1) => {
    553. const color = randomColor({ limitWhite: true });
    554. return {
    555. size: 300 + size * 100,
    556. starLife: 900 + size * 200,
    557. starLifeVariation: 0.22,
    558. color,
    559. crossette: true,
    560. pistil: Math.random() < 0.5,
    561. pistilColor: makePistilColor(color)
    562. };
    563. };
    564. const floralShell = (size = 1) => ({
    565. size: 300 + size * 120,
    566. starDensity: 0.38,
    567. starLife: 500 + size * 50,
    568. starLifeVariation: 0.5,
    569. color: Math.random() < 0.65 ? 'random' : (Math.random() < 0.15 ? randomColor() : [randomColor(), randomColor({ notSame: true })]),
    570. floral: true
    571. });
    572. const fallingLeavesShell = (size = 1) => ({
    573. color: INVISIBLE,
    574. size: 300 + size * 120,
    575. starDensity: 0.38,
    576. starLife: 500 + size * 50,
    577. starLifeVariation: 0.5,
    578. glitter: 'medium',
    579. glitterColor: COLOR.Gold,
    580. fallingLeaves: true
    581. });
    582. const willowShell = (size = 1) => ({
    583. size: 300 + size * 100,
    584. starDensity: 0.7,
    585. starLife: 3000 + size * 300,
    586. glitter: 'willow',
    587. glitterColor: COLOR.Gold,
    588. color: INVISIBLE
    589. });
    590. const crackleShell = (size = 1) => {
    591. // favor gold
    592. const color = Math.random() < 0.75 ? COLOR.Gold : randomColor();
    593. return {
    594. size: 380 + size * 75,
    595. starDensity: 1,
    596. starLife: 600 + size * 100,
    597. starLifeVariation: 0.32,
    598. glitter: 'light',
    599. glitterColor: COLOR.Gold,
    600. color,
    601. crackle: true,
    602. pistil: Math.random() < 0.65,
    603. pistilColor: makePistilColor(color)
    604. };
    605. };
    606. const horsetailShell = (size = 1) => {
    607. const color = randomColor();
    608. return {
    609. horsetail: true,
    610. color,
    611. size: 250 + size * 38,
    612. starDensity: 0.85 + size * 0.1,
    613. starLife: 2500 + size * 300,
    614. glitter: 'medium',
    615. glitterColor: Math.random() < 0.5 ? whiteOrGold() : color
    616. };
    617. };
    618. function randomShellName() {
    619. return Math.random() < 0.6 ? 'Crysanthemum' : shellNames[(Math.random() * (shellNames.length - 1) + 1) | 0];
    620. }
    621. function randomShell(size) {
    622. return shellTypes[randomShellName()](size);
    623. }
    624. function shellFromConfig(size) {
    625. return shellTypes[shellNameSelector()](size);
    626. }
    627. // Get a random shell, not including processing intensive varients
    628. // Note this is only random when "Random" shell is selected in config.
    629. // Also, this does not create the shell, only returns the factory function.
    630. const fastShellBlacklist = ['Falling Leaves', 'Floral', 'Willow'];
    631. function randomFastShell() {
    632. const isRandom = shellNameSelector() === 'Random';
    633. let shellName = isRandom ? randomShellName() : shellNameSelector();
    634. if (isRandom) {
    635. while (fastShellBlacklist.includes(shellName)) {
    636. shellName = randomShellName();
    637. }
    638. }
    639. return shellTypes[shellName];
    640. }
    641. const shellTypes = {
    642. 'Random': randomShell,
    643. 'Crackle': crackleShell,
    644. 'Crossette': crossetteShell,
    645. 'Crysanthemum': crysanthemumShell,
    646. 'Falling Leaves': fallingLeavesShell,
    647. 'Floral': floralShell,
    648. 'Horse Tail': horsetailShell,
    649. 'Palm': palmShell,
    650. 'Ring': ringShell,
    651. 'Willow': willowShell
    652. };
    653. const shellNames = Object.keys(shellTypes);
    654. function init() {
    655. // Populate dropdowns
    656. // shell type
    657. let options = '';
    658. shellNames.forEach(opt => options += `">${opt}`);
    659. appNodes.shellType.innerHTML = options;
    660. // shell size
    661. options = '';
    662. ['3"', '5"', '6"', '8"', '12"'].forEach((opt, i) => options += `">${opt}`);
    663. appNodes.shellSize.innerHTML = options;
    664. // initial render
    665. renderApp(store.state);
    666. }
    667. function fitShellPositionInBoundsH(position) {
    668. const edge = 0.18;
    669. return (1 - edge * 2) * position + edge;
    670. }
    671. function fitShellPositionInBoundsV(position) {
    672. return position * 0.75;
    673. }
    674. function getRandomShellPositionH() {
    675. return fitShellPositionInBoundsH(Math.random());
    676. }
    677. function getRandomShellPositionV() {
    678. return fitShellPositionInBoundsV(Math.random());
    679. }
    680. function getRandomShellSize() {
    681. const baseSize = shellSizeSelector();
    682. const maxVariance = Math.min(2.5, baseSize);
    683. const variance = Math.random() * maxVariance;
    684. const size = baseSize - variance;
    685. const height = maxVariance === 0 ? Math.random() : 1 - (variance / maxVariance);
    686. const centerOffset = Math.random() * (1 - height * 0.65) * 0.5;
    687. const x = Math.random() < 0.5 ? 0.5 - centerOffset : 0.5 + centerOffset;
    688. return {
    689. size,
    690. x: fitShellPositionInBoundsH(x),
    691. height: fitShellPositionInBoundsV(height)
    692. };
    693. }
    694. // Launches a shell from a user pointer event, based on state.config
    695. function launchShellFromConfig(event) {
    696. const shell = new Shell(shellFromConfig(shellSizeSelector()));
    697. const w = mainStage.width;
    698. const h = mainStage.height;
    699. shell.launch(
    700. event ? event.x / w : getRandomShellPositionH(),
    701. event ? 1 - event.y / h : getRandomShellPositionV()
    702. );
    703. }
    704. // Sequences
    705. // -----------
    706. function seqRandomShell() {
    707. const size = getRandomShellSize();
    708. const shell = new Shell(shellFromConfig(size.size));
    709. shell.launch(size.x, size.height);
    710. let extraDelay = shell.starLife;
    711. if (shell.fallingLeaves) {
    712. extraDelay = 4000;
    713. }
    714. return 900 + Math.random() * 600 + extraDelay;
    715. }
    716. function seqTwoRandom() {
    717. const size1 = getRandomShellSize();
    718. const size2 = getRandomShellSize();
    719. const shell1 = new Shell(shellFromConfig(size1.size));
    720. const shell2 = new Shell(shellFromConfig(size2.size));
    721. const leftOffset = Math.random() * 0.2 - 0.1;
    722. const rightOffset = Math.random() * 0.2 - 0.1;
    723. shell1.launch(0.3 + leftOffset, size1.height);
    724. shell2.launch(0.7 + rightOffset, size2.height);
    725. let extraDelay = Math.max(shell1.starLife, shell2.starLife);
    726. if (shell1.fallingLeaves || shell2.fallingLeaves) {
    727. extraDelay = 4000;
    728. }
    729. return 900 + Math.random() * 600 + extraDelay;
    730. }
    731. function seqTriple() {
    732. const shellType = randomFastShell();
    733. const baseSize = shellSizeSelector();
    734. const smallSize = Math.max(0, baseSize - 1.25);
    735. const offset = Math.random() * 0.08 - 0.04;
    736. const shell1 = new Shell(shellType(baseSize));
    737. shell1.launch(0.5 + offset, 0.7);
    738. const leftDelay = 1000 + Math.random() * 400;
    739. const rightDelay = 1000 + Math.random() * 400;
    740. setTimeout(() => {
    741. const offset = Math.random() * 0.08 - 0.04;
    742. const shell2 = new Shell(shellType(smallSize));
    743. shell2.launch(0.2 + offset, 0.1);
    744. }, leftDelay);
    745. setTimeout(() => {
    746. const offset = Math.random() * 0.08 - 0.04;
    747. const shell3 = new Shell(shellType(smallSize));
    748. shell3.launch(0.8 + offset, 0.1);
    749. }, rightDelay);
    750. return 4000;
    751. }
    752. function seqSmallBarrage() {
    753. seqSmallBarrage.lastCalled = Date.now();
    754. const barrageCount = IS_DESKTOP ? 11 : 5;
    755. const shellSize = Math.max(0, shellSizeSelector() - 2);
    756. const useCrysanthemum = Math.random() < 0.7;
    757. // (cos(x*5π+0.5π)+1)/2 is a custom wave bounded by 0 and 1 used to set varying launch heights
    758. function launchShell(x) {
    759. const isRandom = shellNameSelector() === 'Random';
    760. let shellType = isRandom ? (useCrysanthemum ? crysanthemumShell : randomFastShell()) : shellTypes[shellNameSelector()];
    761. const shell = new Shell(shellType(shellSize));
    762. const height = (Math.cos(x * 5 * Math.PI + PI_HALF) + 1) / 2;
    763. shell.launch(x, height * 0.75);
    764. }
    765. let count = 0;
    766. let delay = 0;
    767. while (count < barrageCount) {
    768. if (count === 0) {
    769. launchShell(0.5)
    770. count += 1;
    771. }
    772. else {
    773. const offset = (count + 1) / barrageCount / 2;
    774. setTimeout(() => {
    775. launchShell(0.5 + offset);
    776. launchShell(0.5 - offset);
    777. }, delay);
    778. count += 2;
    779. }
    780. delay += 200;
    781. }
    782. return 3400 + barrageCount * 120;
    783. }
    784. seqSmallBarrage.cooldown = 15000;
    785. seqSmallBarrage.lastCalled = Date.now();
    786. const sequences = [
    787. seqRandomShell,
    788. seqTwoRandom,
    789. seqTriple,
    790. seqSmallBarrage
    791. ];
    792. let isFirstSeq = true;
    793. const finaleCount = 32;
    794. let currentFinaleCount = 0;
    795. function startSequence() {
    796. if (isFirstSeq) {
    797. isFirstSeq = false;
    798. const shell = new Shell(crysanthemumShell(shellSizeSelector()));
    799. shell.launch(0.5, 0.5);
    800. return 2400;
    801. }
    802. if (finaleSelector()) {
    803. seqRandomShell();
    804. if (currentFinaleCount < finaleCount) {
    805. currentFinaleCount++;
    806. return 170;
    807. }
    808. else {
    809. currentFinaleCount = 0;
    810. return 6000;
    811. }
    812. }
    813. const rand = Math.random();
    814. if (rand < 0.2 && Date.now() - seqSmallBarrage.lastCalled > seqSmallBarrage.cooldown) {
    815. return seqSmallBarrage();
    816. }
    817. if (rand < 0.6) {
    818. return seqRandomShell();
    819. }
    820. else if (rand < 0.8) {
    821. return seqTwoRandom();
    822. }
    823. else if (rand < 1) {
    824. return seqTriple();
    825. }
    826. }
    827. let activePointerCount = 0;
    828. let isUpdatingSpeed = false;
    829. function handlePointerStart(event) {
    830. activePointerCount++;
    831. const btnSize = 44;
    832. if (event.y < btnSize) {
    833. if (event.x < btnSize) {
    834. togglePause();
    835. return;
    836. }
    837. if (event.x > mainStage.width / 2 - btnSize / 2 && event.x < mainStage.width / 2 + btnSize / 2) {
    838. toggleLongExposure();
    839. return;
    840. }
    841. if (event.x > mainStage.width - btnSize) {
    842. toggleMenu();
    843. return;
    844. }
    845. }
    846. if (!canInteract()) return;
    847. if (updateSpeedFromEvent(event)) {
    848. isUpdatingSpeed = true;
    849. }
    850. else if (event.onCanvas) {
    851. launchShellFromConfig(event);
    852. }
    853. }
    854. function handlePointerEnd(event) {
    855. activePointerCount--;
    856. isUpdatingSpeed = false;
    857. }
    858. function handlePointerMove(event) {
    859. if (!canInteract()) return;
    860. if (isUpdatingSpeed) {
    861. updateSpeedFromEvent(event);
    862. }
    863. }
    864. function handleKeydown(event) {
    865. // P
    866. if (event.keyCode === 80) {
    867. togglePause();
    868. }
    869. // O
    870. else if (event.keyCode === 79) {
    871. toggleMenu();
    872. }
    873. // Esc
    874. else if (event.keyCode === 27) {
    875. toggleMenu(false);
    876. }
    877. }
    878. mainStage.addEventListener('pointerstart', handlePointerStart);
    879. mainStage.addEventListener('pointerend', handlePointerEnd);
    880. mainStage.addEventListener('pointermove', handlePointerMove);
    881. window.addEventListener('keydown', handleKeydown);
    882. // Try to go fullscreen upon a touch
    883. window.addEventListener('touchend', (event) => !IS_DESKTOP && requestFullscreen());
    884. function handleResize() {
    885. const w = window.innerWidth;
    886. const h = window.innerHeight;
    887. // Try to adopt screen size, heeding maximum sizes specified
    888. const containerW = Math.min(w, MAX_WIDTH);
    889. // On small screens, use full device height
    890. const containerH = w <= 420 ? h : Math.min(h, MAX_HEIGHT);
    891. appNodes.stageContainer.style.width = containerW + 'px';
    892. appNodes.stageContainer.style.height = containerH + 'px';
    893. stages.forEach(stage => stage.resize(containerW, containerH));
    894. }
    895. // Compute initial dimensions
    896. handleResize();
    897. window.addEventListener('resize', handleResize);
    898. // Dynamic globals
    899. let speedBarOpacity = 0;
    900. let autoLaunchTime = 0;
    901. function updateSpeedFromEvent(event) {
    902. if (isUpdatingSpeed || event.y >= mainStage.height - 44) {
    903. // On phones it's hard to hit the edge pixels in order to set speed at 0 or 1, so some padding is provided to make that easier.
    904. const edge = 16;
    905. const newSpeed = (event.x - edge) / (mainStage.width - edge * 2);
    906. simSpeed = Math.min(Math.max(newSpeed, 0), 1);
    907. // show speed bar after an update
    908. speedBarOpacity = 1;
    909. // If we updated the speed, return true
    910. return true;
    911. }
    912. // Return false if the speed wasn't updated
    913. return false;
    914. }
    915. // Extracted function to keep `update()` optimized
    916. function updateGlobals(timeStep, lag) {
    917. // Always try to fade out speed bar
    918. if (!isUpdatingSpeed) {
    919. speedBarOpacity -= lag / 30; // half a second
    920. if (speedBarOpacity < 0) {
    921. speedBarOpacity = 0;
    922. }
    923. }
    924. // auto launch shells
    925. if (store.state.config.autoLaunch) {
    926. autoLaunchTime -= timeStep;
    927. if (autoLaunchTime <= 0) {
    928. autoLaunchTime = startSequence();
    929. }
    930. }
    931. }
    932. function update(frameTime, lag) {
    933. if (!canInteract()) return;
    934. const { width, height } = mainStage;
    935. const timeStep = frameTime * simSpeed;
    936. const speed = simSpeed * lag;
    937. updateGlobals(timeStep, lag);
    938. const starDrag = 1 - (1 - Star.airDrag) * speed;
    939. const starDragHeavy = 1 - (1 - Star.airDragHeavy) * speed;
    940. const sparkDrag = 1 - (1 - Spark.airDrag) * speed;
    941. const gAcc = timeStep / 1000 * GRAVITY;
    942. COLOR_CODES_W_INVIS.forEach(color => {
    943. // Stars
    944. Star.active[color].forEach((star, i, stars) => {
    945. star.life -= timeStep;
    946. if (star.life <= 0) {
    947. stars.splice(i, 1);
    948. Star.returnInstance(star);
    949. } else {
    950. star.prevX = star.x;
    951. star.prevY = star.y;
    952. star.x += star.speedX * speed;
    953. star.y += star.speedY * speed;
    954. // Apply air drag if star isn't "heavy". The heavy property is used for the shell comets.
    955. if (!star.heavy) {
    956. star.speedX *= starDrag;
    957. star.speedY *= starDrag;
    958. }
    959. else {
    960. star.speedX *= starDragHeavy;
    961. star.speedY *= starDragHeavy;
    962. }
    963. star.speedY += gAcc;
    964. if (star.spinRadius) {
    965. star.spinAngle += star.spinSpeed * speed;
    966. star.x += Math.sin(star.spinAngle) * star.spinRadius * speed;
    967. star.y += Math.cos(star.spinAngle) * star.spinRadius * speed;
    968. }
    969. if (star.sparkFreq) {
    970. star.sparkTimer -= timeStep;
    971. while (star.sparkTimer < 0) {
    972. star.sparkTimer += star.sparkFreq;
    973. Spark.add(
    974. star.x,
    975. star.y,
    976. star.sparkColor,
    977. Math.random() * PI_2,
    978. Math.random() * star.sparkSpeed,
    979. star.sparkLife * 0.8 + Math.random() * star.sparkLifeVariation * star.sparkLife
    980. );
    981. }
    982. }
    983. }
    984. });
    985. // Sparks
    986. Spark.active[color].forEach((spark, i, sparks) => {
    987. spark.life -= timeStep;
    988. if (spark.life <= 0) {
    989. sparks.splice(i, 1);
    990. Spark.returnInstance(spark);
    991. } else {
    992. spark.prevX = spark.x;
    993. spark.prevY = spark.y;
    994. spark.x += spark.speedX * speed;
    995. spark.y += spark.speedY * speed;
    996. spark.speedX *= sparkDrag;
    997. spark.speedY *= sparkDrag;
    998. spark.speedY += gAcc;
    999. }
    1000. });
    1001. });
    1002. render(speed);
    1003. }
    1004. function render(speed) {
    1005. const { dpr, width, height } = mainStage;
    1006. const trailsCtx = trailsStage.ctx;
    1007. const mainCtx = mainStage.ctx;
    1008. colorSky(speed);
    1009. trailsCtx.scale(dpr, dpr);
    1010. mainCtx.scale(dpr, dpr);
    1011. trailsCtx.globalCompositeOperation = 'source-over';
    1012. trailsCtx.fillStyle = `rgba(0, 0, 0, ${store.state.longExposure ? 0.0025 : 0.1 * speed})`;
    1013. trailsCtx.fillRect(0, 0, width, height);
    1014. // Remaining drawing on trails canvas will use 'lighten' blend mode
    1015. trailsCtx.globalCompositeOperation = 'lighten';
    1016. mainCtx.clearRect(0, 0, width, height);
    1017. // Draw queued burst flashes
    1018. while (BurstFlash.active.length) {
    1019. const bf = BurstFlash.active.pop();
    1020. const burstGradient = trailsCtx.createRadialGradient(bf.x, bf.y, 0, bf.x, bf.y, bf.radius);
    1021. burstGradient.addColorStop(0.05, 'white');
    1022. burstGradient.addColorStop(0.25, 'rgba(255, 160, 20, 0.2)');
    1023. burstGradient.addColorStop(1, 'rgba(255, 160, 20, 0)');
    1024. trailsCtx.fillStyle = burstGradient;
    1025. trailsCtx.fillRect(bf.x - bf.radius, bf.y - bf.radius, bf.radius * 2, bf.radius * 2);
    1026. BurstFlash.returnInstance(bf);
    1027. }
    1028. // Draw stars
    1029. trailsCtx.lineWidth = Star.drawWidth;
    1030. trailsCtx.lineCap = 'round';
    1031. mainCtx.strokeStyle = '#fff';
    1032. mainCtx.lineWidth = 1;
    1033. mainCtx.beginPath();
    1034. COLOR_CODES.forEach(color => {
    1035. const stars = Star.active[color];
    1036. trailsCtx.strokeStyle = color;
    1037. trailsCtx.beginPath();
    1038. stars.forEach(star => {
    1039. trailsCtx.moveTo(star.x, star.y);
    1040. trailsCtx.lineTo(star.prevX, star.prevY);
    1041. mainCtx.moveTo(star.x, star.y);
    1042. mainCtx.lineTo(star.x - star.speedX * 1.6, star.y - star.speedY * 1.6);
    1043. });
    1044. trailsCtx.stroke();
    1045. });
    1046. mainCtx.stroke();
    1047. // Draw sparks
    1048. trailsCtx.lineWidth = Spark.drawWidth;
    1049. trailsCtx.lineCap = 'butt';
    1050. COLOR_CODES.forEach(color => {
    1051. const sparks = Spark.active[color];
    1052. trailsCtx.strokeStyle = color;
    1053. trailsCtx.beginPath();
    1054. sparks.forEach(spark => {
    1055. trailsCtx.moveTo(spark.x, spark.y);
    1056. trailsCtx.lineTo(spark.prevX, spark.prevY);
    1057. });
    1058. trailsCtx.stroke();
    1059. });
    1060. // Render speed bar if visible
    1061. if (speedBarOpacity) {
    1062. const speedBarHeight = 6;
    1063. mainCtx.globalAlpha = speedBarOpacity;
    1064. mainCtx.fillStyle = COLOR.Blue;
    1065. mainCtx.fillRect(0, height - speedBarHeight, width * simSpeed, speedBarHeight);
    1066. mainCtx.globalAlpha = 1;
    1067. }
    1068. trailsCtx.resetTransform();
    1069. mainCtx.resetTransform();
    1070. }
    1071. // Draw colored overlay based on combined brightness of stars (light up the sky!)
    1072. // Note: this is applied to the canvas container's background-color, so it's behind the particles
    1073. const currentSkyColor = { r: 0, g: 0, b: 0 };
    1074. const targetSkyColor = { r: 0, g: 0, b: 0 };
    1075. function colorSky(speed) {
    1076. // The maximum r, g, or b value that will be used (255 would represent no maximum)
    1077. const maxSkySaturation = 30;
    1078. // How many stars are required in total to reach maximum sky brightness
    1079. const maxStarCount = 500;
    1080. let totalStarCount = 0;
    1081. // Initialize sky as black
    1082. targetSkyColor.r = 0;
    1083. targetSkyColor.g = 0;
    1084. targetSkyColor.b = 0;
    1085. // Add each known color to sky, multiplied by particle count of that color. This will put RGB values wildly out of bounds, but we'll scale them back later.
    1086. // Also add up total star count.
    1087. COLOR_CODES.forEach(color => {
    1088. const tuple = COLOR_TUPLES[color];
    1089. const count = Star.active[color].length;
    1090. totalStarCount += count;
    1091. targetSkyColor.r += tuple.r * count;
    1092. targetSkyColor.g += tuple.g * count;
    1093. targetSkyColor.b += tuple.b * count;
    1094. });
    1095. // Clamp intensity at 1.0, and map to a custom non-linear curve. This allows few stars to perceivably light up the sky, while more stars continue to increase the brightness but at a lesser rate. This is more inline with humans' non-linear brightness perception.
    1096. const intensity = Math.pow(Math.min(1, totalStarCount / maxStarCount), 0.3);
    1097. // Figure out which color component has the highest value, so we can scale them without affecting the ratios.
    1098. // Prevent 0 from being used, so we don't divide by zero in the next step.
    1099. const maxColorComponent = Math.max(1, targetSkyColor.r, targetSkyColor.g, targetSkyColor.b);
    1100. // Scale all color components to a max of `maxSkySaturation`, and apply intensity.
    1101. targetSkyColor.r = targetSkyColor.r / maxColorComponent * maxSkySaturation * intensity;
    1102. targetSkyColor.g = targetSkyColor.g / maxColorComponent * maxSkySaturation * intensity;
    1103. targetSkyColor.b = targetSkyColor.b / maxColorComponent * maxSkySaturation * intensity;
    1104. // Animate changes to color to smooth out transitions.
    1105. const colorChange = 10;
    1106. currentSkyColor.r += (targetSkyColor.r - currentSkyColor.r) / colorChange * speed;
    1107. currentSkyColor.g += (targetSkyColor.g - currentSkyColor.g) / colorChange * speed;
    1108. currentSkyColor.b += (targetSkyColor.b - currentSkyColor.b) / colorChange * speed;
    1109. appNodes.canvasContainer.style.backgroundColor = `rgb(${currentSkyColor.r | 0}, ${currentSkyColor.g | 0}, ${currentSkyColor.b | 0})`;
    1110. }
    1111. mainStage.addEventListener('ticker', update);
    1112. // Helper used to semi-randomly spread particles over an arc
    1113. // Values are flexible - `start` and `arcLength` can be negative, and `randomness` is simply a multiplier for random addition.
    1114. function createParticleArc(start, arcLength, count, randomness, particleFactory) {
    1115. const angleDelta = arcLength / count;
    1116. // Sometimes there is an extra particle at the end, too close to the start. Subtracting half the angleDelta ensures that is skipped.
    1117. // Would be nice to fix this a better way.
    1118. const end = start + arcLength - (angleDelta * 0.5);
    1119. if (end > start) {
    1120. // Optimization: `angle=angle+angleDelta` vs. angle+=angleDelta
    1121. // V8 deoptimises with let compound assignment
    1122. for (let angle = start; angle < end; angle = angle + angleDelta) {
    1123. particleFactory(angle + Math.random() * angleDelta * randomness);
    1124. }
    1125. }
    1126. else {
    1127. for (let angle = start; angle > end; angle = angle + angleDelta) {
    1128. particleFactory(angle + Math.random() * angleDelta * randomness);
    1129. }
    1130. }
    1131. }
    1132. // Various star effects.
    1133. // These are designed to be attached to a star's `onDeath` event.
    1134. // Crossette breaks star into four same-color pieces which branch in a cross-like shape.
    1135. function crossetteEffect(star) {
    1136. const startAngle = Math.random() * PI_HALF;
    1137. createParticleArc(startAngle, PI_2, 4, 0.5, (angle) => {
    1138. Star.add(
    1139. star.x,
    1140. star.y,
    1141. star.color,
    1142. angle,
    1143. Math.random() * 0.6 + 0.75,
    1144. 600
    1145. );
    1146. });
    1147. }
    1148. // Flower is like a mini shell
    1149. function floralEffect(star) {
    1150. const startAngle = Math.random() * PI_HALF;
    1151. createParticleArc(startAngle, PI_2, 24, 1, (angle) => {
    1152. Star.add(
    1153. star.x,
    1154. star.y,
    1155. star.color,
    1156. angle,
    1157. // apply near cubic falloff to speed (places more particles towards outside)
    1158. Math.pow(Math.random(), 0.45) * 2.4,
    1159. 1000 + Math.random() * 300,
    1160. star.speedX,
    1161. star.speedY
    1162. );
    1163. });
    1164. // Queue burst flash render
    1165. BurstFlash.add(star.x, star.y, 24);
    1166. }
    1167. // Floral burst with willow stars
    1168. function fallingLeavesEffect(star) {
    1169. const startAngle = Math.random() * PI_HALF;
    1170. createParticleArc(startAngle, PI_2, 12, 1, (angle) => {
    1171. const newStar = Star.add(
    1172. star.x,
    1173. star.y,
    1174. INVISIBLE,
    1175. angle,
    1176. // apply near cubic falloff to speed (places more particles towards outside)
    1177. Math.pow(Math.random(), 0.45) * 2.4,
    1178. 2400 + Math.random() * 600,
    1179. star.speedX,
    1180. star.speedY
    1181. );
    1182. newStar.sparkColor = COLOR.Gold;
    1183. newStar.sparkFreq = 72;
    1184. newStar.sparkSpeed = 0.28;
    1185. newStar.sparkLife = 750;
    1186. newStar.sparkLifeVariation = 3.2;
    1187. });
    1188. // Queue burst flash render
    1189. BurstFlash.add(star.x, star.y, 24);
    1190. }
    1191. // Crackle pops into a small cloud of golden sparks.
    1192. function crackleEffect(star) {
    1193. createParticleArc(0, PI_2, 10, 1.8, (angle) => {
    1194. Spark.add(
    1195. star.x,
    1196. star.y,
    1197. COLOR.Gold,
    1198. angle,
    1199. // apply near cubic falloff to speed (places more particles towards outside)
    1200. Math.pow(Math.random(), 0.45) * 2.4,
    1201. 300 + Math.random() * 200
    1202. );
    1203. });
    1204. }
    1205. /**
    1206. * Shell can be constructed with options:
    1207. *
    1208. * size: Size of the burst.
    1209. * starCount: Number of stars to create. This is optional, and will be set to a reasonable quantity for size if omitted.
    1210. * starLife:
    1211. * starLifeVariation:
    1212. * color:
    1213. * glitterColor:
    1214. * glitter: One of: 'light', 'medium', 'heavy', 'streamer', 'willow'
    1215. * pistil:
    1216. * pistilColor:
    1217. * streamers:
    1218. * crossette:
    1219. * floral:
    1220. * crackle:
    1221. */
    1222. class Shell {
    1223. constructor(options) {
    1224. Object.assign(this, options);
    1225. this.starLifeVariation = options.starLifeVariation || 0.125;
    1226. this.color = options.color || randomColor();
    1227. this.glitterColor = options.glitterColor || this.color;
    1228. // Set default starCount if needed, will be based on shell size and scale exponentially, like a sphere's surface area.
    1229. if (!this.starCount) {
    1230. const density = options.starDensity || 1;
    1231. const scaledSize = this.size / 50 * density;
    1232. this.starCount = scaledSize * scaledSize;
    1233. }
    1234. }
    1235. launch(position, launchHeight) {
    1236. const { width, height } = mainStage;
    1237. // Distance from sides of screen to keep shells.
    1238. const hpad = 60;
    1239. // Distance from top of screen to keep shell bursts.
    1240. const vpad = 50;
    1241. // Minimum burst height, as a percentage of stage height
    1242. const minHeightPercent = 0.45;
    1243. // Minimum burst height in px
    1244. const minHeight = height - height * minHeightPercent;
    1245. const launchX = position * (width - hpad * 2) + hpad;
    1246. const launchY = height;
    1247. const burstY = minHeight - (launchHeight * (minHeight - vpad));
    1248. const launchDistance = launchY - burstY;
    1249. // Using a custom power curve to approximate Vi needed to reach launchDistance under gravity and air drag.
    1250. // Magic numbers came from testing.
    1251. const launchVelocity = Math.pow(launchDistance * 0.04, 0.64);
    1252. const comet = this.comet = Star.add(
    1253. launchX,
    1254. launchY,
    1255. typeof this.color === 'string' && this.color !== 'random' ? this.color : COLOR.White,
    1256. Math.PI,
    1257. launchVelocity * (this.horsetail ? 1.2 : 1),
    1258. // Hang time is derived linearly from Vi; exact number came from testing
    1259. launchVelocity * (this.horsetail ? 100 : 400)
    1260. );
    1261. // making comet "heavy" limits air drag
    1262. comet.heavy = true;
    1263. // comet spark trail
    1264. comet.spinRadius = 0.78;
    1265. comet.sparkFreq = 16;
    1266. if (this.glitter === 'willow' || this.fallingLeaves) {
    1267. comet.sparkFreq = 10;
    1268. comet.sparkSpeed = 0.5;
    1269. comet.sparkLife = 500;
    1270. comet.sparkLifeVariation = 3;
    1271. }
    1272. if (this.color === INVISIBLE) {
    1273. comet.sparkColor = COLOR.Gold;
    1274. }
    1275. comet.onDeath = comet => this.burst(comet.x, comet.y);
    1276. // comet.onDeath = () => this.burst(launchX, burstY);
    1277. }
    1278. burst(x, y) {
    1279. // Set burst speed so overall burst grows to set size. This specific formula was derived from testing, and is affected by simulated air drag.
    1280. const speed = this.size / 96;
    1281. let color, onDeath, sparkFreq, sparkSpeed, sparkLife;
    1282. let sparkLifeVariation = 0.25;
    1283. if (this.crossette) onDeath = crossetteEffect;
    1284. if (this.floral) onDeath = floralEffect;
    1285. if (this.crackle) onDeath = crackleEffect;
    1286. if (this.fallingLeaves) onDeath = fallingLeavesEffect;
    1287. if (this.glitter === 'light') {
    1288. sparkFreq = 200;
    1289. sparkSpeed = 0.25;
    1290. sparkLife = 600;
    1291. }
    1292. else if (this.glitter === 'medium') {
    1293. sparkFreq = 100;
    1294. sparkSpeed = 0.36;
    1295. sparkLife = 1400;
    1296. }
    1297. else if (this.glitter === 'heavy') {
    1298. sparkFreq = 42;
    1299. sparkSpeed = 0.62;
    1300. sparkLife = 2800;
    1301. }
    1302. else if (this.glitter === 'streamer') {
    1303. sparkFreq = 20;
    1304. sparkSpeed = 0.75;
    1305. sparkLife = 800;
    1306. }
    1307. else if (this.glitter === 'willow') {
    1308. sparkFreq = 72;
    1309. sparkSpeed = 0.28;
    1310. sparkLife = 1000;
    1311. sparkLifeVariation = 3.4;
    1312. }
    1313. const starFactory = angle => {
    1314. const star = Star.add(
    1315. x,
    1316. y,
    1317. color || randomColor(),
    1318. angle,
    1319. // apply near cubic falloff to speed (places more particles towards outside)
    1320. Math.pow(Math.random(), 0.45) * speed,
    1321. // add minor variation to star life
    1322. this.starLife + Math.random() * this.starLife * this.starLifeVariation,
    1323. this.horsetail && this.comet && this.comet.speedX,
    1324. this.horsetail && this.comet && this.comet.speedY
    1325. );
    1326. star.onDeath = onDeath;
    1327. if (this.glitter) {
    1328. star.sparkFreq = sparkFreq;
    1329. star.sparkSpeed = sparkSpeed;
    1330. star.sparkLife = sparkLife;
    1331. star.sparkLifeVariation = sparkLifeVariation;
    1332. star.sparkColor = this.glitterColor;
    1333. star.sparkTimer = Math.random() * star.sparkFreq;
    1334. }
    1335. };
    1336. if (typeof this.color === 'string') {
    1337. if (this.color === 'random') {
    1338. color = null; // falsey value creates random color in starFactory
    1339. } else {
    1340. color = this.color;
    1341. }
    1342. // Rings have positional randomness, but are rotated randomly
    1343. if (this.ring) {
    1344. const ringStartAngle = Math.random() * Math.PI;
    1345. const ringSquash = Math.pow(Math.random(), 0.45) * 0.992 + 0.008;
    1346. createParticleArc(0, PI_2, this.starCount, 0, angle => {
    1347. // Create a ring, squashed horizontally
    1348. const initSpeedX = Math.sin(angle) * speed * ringSquash;
    1349. const initSpeedY = Math.cos(angle) * speed;
    1350. // Rotate ring
    1351. const newSpeed = MyMath.pointDist(0, 0, initSpeedX, initSpeedY);
    1352. const newAngle = MyMath.pointAngle(0, 0, initSpeedX, initSpeedY) + ringStartAngle;
    1353. const star = Star.add(
    1354. x,
    1355. y,
    1356. color,
    1357. newAngle,
    1358. // apply near cubic falloff to speed (places more particles towards outside)
    1359. newSpeed,//speed,
    1360. // add minor variation to star life
    1361. this.starLife + Math.random() * this.starLife * this.starLifeVariation
    1362. );
    1363. if (this.glitter) {
    1364. star.sparkFreq = sparkFreq;
    1365. star.sparkSpeed = sparkSpeed;
    1366. star.sparkLife = sparkLife;
    1367. star.sparkLifeVariation = sparkLifeVariation;
    1368. star.sparkColor = this.glitterColor;
    1369. star.sparkTimer = Math.random() * star.sparkFreq;
    1370. }
    1371. });
    1372. }
    1373. // "Normal burst
    1374. else {
    1375. createParticleArc(0, PI_2, this.starCount, 1, starFactory);
    1376. }
    1377. }
    1378. else if (Array.isArray(this.color)) {
    1379. let start, start2, arc;
    1380. if (Math.random() < 0.5) {
    1381. start = Math.random() * Math.PI;
    1382. start2 = start + Math.PI;
    1383. arc = Math.PI;
    1384. } else {
    1385. start = 0;
    1386. start2 = 0;
    1387. arc = PI_2;
    1388. }
    1389. color = this.color[0];
    1390. createParticleArc(start, arc, this.starCount / 2, 1, starFactory);
    1391. color = this.color[1];
    1392. createParticleArc(start2, arc, this.starCount / 2, 1, starFactory)
    1393. }
    1394. if (this.pistil) {
    1395. const innerShell = new Shell({
    1396. size: this.size * 0.5,
    1397. starLife: this.starLife * 0.7,
    1398. starLifeVariation: this.starLifeVariation,
    1399. starDensity: 1.65,
    1400. color: this.pistilColor,
    1401. glitter: 'light',
    1402. glitterColor: this.pistilColor === COLOR.Gold ? COLOR.Gold : COLOR.White
    1403. });
    1404. innerShell.burst(x, y);
    1405. }
    1406. if (this.streamers) {
    1407. const innerShell = new Shell({
    1408. size: this.size,
    1409. starLife: this.starLife * 0.8,
    1410. starLifeVariation: this.starLifeVariation,
    1411. starCount: Math.max(6, this.size / 45) | 0,
    1412. color: COLOR.White,
    1413. glitter: 'streamer'
    1414. });
    1415. innerShell.burst(x, y);
    1416. }
    1417. // Queue burst flash render
    1418. BurstFlash.add(x, y, this.size / 8);
    1419. }
    1420. }
    1421. const BurstFlash = {
    1422. active: [],
    1423. _pool: [],
    1424. _new() {
    1425. return {}
    1426. },
    1427. add(x, y, radius) {
    1428. const instance = this._pool.pop() || this._new();
    1429. instance.x = x;
    1430. instance.y = y;
    1431. instance.radius = radius;
    1432. this.active.push(instance);
    1433. return instance;
    1434. },
    1435. returnInstance(instance) {
    1436. this._pool.push(instance);
    1437. }
    1438. };
    1439. // Helper to generate objects for storing active particles.
    1440. // Particles are stored in arrays keyed by color (code, not name) for improved rendering performance.
    1441. function createParticleCollection() {
    1442. const collection = {};
    1443. COLOR_CODES_W_INVIS.forEach(color => {
    1444. collection[color] = [];
    1445. });
    1446. return collection;
    1447. }
    1448. const Star = {
    1449. // Visual properties
    1450. drawWidth: 3,
    1451. airDrag: 0.98,
    1452. airDragHeavy: 0.992,
    1453. // Star particles will be keyed by color
    1454. active: createParticleCollection(),
    1455. _pool: [],
    1456. _new() {
    1457. return {};
    1458. },
    1459. add(x, y, color, angle, speed, life, speedOffX, speedOffY) {
    1460. const instance = this._pool.pop() || this._new();
    1461. instance.heavy = false;
    1462. instance.x = x;
    1463. instance.y = y;
    1464. instance.prevX = x;
    1465. instance.prevY = y;
    1466. instance.color = color;
    1467. instance.speedX = Math.sin(angle) * speed + (speedOffX || 0);
    1468. instance.speedY = Math.cos(angle) * speed + (speedOffY || 0);
    1469. instance.life = life;
    1470. instance.spinAngle = Math.random() * PI_2;
    1471. instance.spinSpeed = 0.8;
    1472. instance.spinRadius = 0;
    1473. instance.sparkFreq = 0; // ms between spark emissions
    1474. instance.sparkSpeed = 1;
    1475. instance.sparkTimer = 0;
    1476. instance.sparkColor = color;
    1477. instance.sparkLife = 750;
    1478. instance.sparkLifeVariation = 0.25;
    1479. this.active[color].push(instance);
    1480. return instance;
    1481. },
    1482. // Public method for cleaning up and returning an instance back to the pool.
    1483. returnInstance(instance) {
    1484. // Call onDeath handler if available (and pass it current star instance)
    1485. instance.onDeath && instance.onDeath(instance);
    1486. // Clean up
    1487. instance.onDeath = null;
    1488. // Add back to the pool.
    1489. this._pool.push(instance);
    1490. }
    1491. };
    1492. const Spark = {
    1493. // Visual properties
    1494. drawWidth: 0.75,
    1495. airDrag: 0.9,
    1496. // Star particles will be keyed by color
    1497. active: createParticleCollection(),
    1498. _pool: [],
    1499. _new() {
    1500. return {};
    1501. },
    1502. add(x, y, color, angle, speed, life) {
    1503. const instance = this._pool.pop() || this._new();
    1504. instance.x = x;
    1505. instance.y = y;
    1506. instance.prevX = x;
    1507. instance.prevY = y;
    1508. instance.color = color;
    1509. instance.speedX = Math.sin(angle) * speed;
    1510. instance.speedY = Math.cos(angle) * speed;
    1511. instance.life = life;
    1512. this.active[color].push(instance);
    1513. return instance;
    1514. },
    1515. // Public method for cleaning up and returning an instance back to the pool.
    1516. returnInstance(instance) {
    1517. // Add back to the pool.
    1518. this._pool.push(instance);
    1519. }
    1520. };
    1521. init();
    1522. script>
    1523. body>
    1524. html>

    二、代码原理

    这段代码的实现原理主要是通过 HTML、CSS 和外部资源(图标、字体和样式表)来构建一个具有烟花秀效果的网页。

    首先,通过标签设置文档的元数据,如字符编码、视口大小等。然后,通过标签引入外部资源,包括图标、字体和样式表。