当前位置:   article > 正文

2024年新春快乐跨年烟花代码(小白操作)_新年快乐代码特效2024

新年快乐代码特效2024

全屏烟花动画特效,跨年新春烟花漫天效果。无加密,完整可用哦

小白操作流程

1.桌面新建文本文档.txt

2.复制粘贴下面的代码

3.保存文件并修改文件的后缀为.html

4.点开生成的html文件就可以放一场电子烟花

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/85149
推荐阅读
相关标签
  

闽ICP备14008679号