前端背景收集之烟花背景

🐒个人主页:信计2102罗铠威

🏅Vue项目常用组件模板仓库

📖前言:

本篇博客主要提供前端背景收集之烟花背景组件源码,需要的朋友请自取
在这里插入图片描述

在这里插入图片描述

🎀源码如下:

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>2024新年快乐!万事如意!</title>
		<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
		<meta name="mobile-web-app-capable" content="yes">
		<meta name="apple-mobile-web-app-capable" content="yes">
		<meta name="theme-color" content="#000000">
		<link rel="shortcut icon" type="image/png"
			href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
		<link rel="icon" type="image/png"
			href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
		<link rel="apple-touch-icon-precomposed"
			href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
		<meta name="msapplication-TileColor" content="#000000">
		<meta name="msapplication-TileImage"
			content="https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/firework-burst-icon.png">
		<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">
		<link rel="stylesheet" href="./style.css">
		<style>
			* {
				position: relative;
				box-sizing: border-box;
			}

			html,
			body {
				height: 100%;
			}

			html {
				background-color: #000;
			}

			body {
				overflow: hidden;
				color: rgba(255, 255, 255, 0.5);
				font-family: "Russo One", arial, sans-serif;
				line-height: 1.25;
				letter-spacing: 0.06em;
			}

			.hide {
				opacity: 0;
				visibility: hidden;
			}

			.remove {
				display: none;
			}

			.blur {
				filter: blur(12px);
			}

			.container {
				height: 100%;
				display: flex;
				justify-content: center;
				align-items: center;
			}

			#loading-init {
				width: 100%;
				align-self: center;
				text-align: center;
				font-size: 2em;
			}

			#stage-container {
				overflow: hidden;
				box-sizing: initial;
				border: 1px solid #222;
				margin: -1px;
			}

			#canvas-container {
				width: 100%;
				height: 100%;
				transition: filter 0.3s;
			}

			#canvas-container canvas {
				position: absolute;
				mix-blend-mode: lighten;
			}

			#controls {
				position: absolute;
				top: 0;
				width: 100%;
				padding-bottom: 50px;
				display: flex;
				justify-content: space-between;
				transition: opacity 0.3s, visibility 0.3s;
			}

			@media (min-width: 800px) {
				#controls {
					visibility: visible;
				}

				#controls.hide:hover {
					opacity: 1;
				}
			}

			#menu {
				display: flex;
				flex-direction: column;
				justify-content: center;
				align-items: center;
				position: absolute;
				top: 0;
				bottom: 0;
				width: 100%;
				background-color: rgba(0, 0, 0, 0.42);
				transition: opacity 0.3s, visibility 0.3s;
			}

			#menu__header {
				padding: 20px 0 44px;
				font-size: 2em;
				text-transform: uppercase;
			}

			#menu form {
				width: 240px;
				padding: 0 20px;
				overflow: auto;
			}

			#menu .form-option {
				margin: 20px 0;
			}

			#menu .form-option label {
				text-transform: uppercase;
			}

			#menu .form-option--select label {
				display: block;
				margin-bottom: 6px;
			}

			#menu .form-option--select select {
				display: block;
				width: 100%;
				height: 30px;
				font-size: 1rem;
				font-family: "Russo One", arial, sans-serif;
				color: rgba(255, 255, 255, 0.5);
				letter-spacing: 0.06em;
				background-color: transparent;
				border: 1px solid rgba(255, 255, 255, 0.5);
			}

			#menu .form-option--select select option {
				background-color: black;
			}

			#menu .form-option--checkbox label {
				display: flex;
				align-items: center;
				transition: opacity 0.3s;
				-webkit-user-select: none;
				-moz-user-select: none;
				-ms-user-select: none;
				user-select: none;
			}

			#menu .form-option--checkbox input {
				display: block;
				width: 20px;
				height: 20px;
				margin-right: 8px;
				opacity: 0.5;
			}

			@media (max-width: 800px) {

				#menu .form-option select,
				#menu .form-option input {
					outline: none;
				}
			}

			#close-menu-btn {
				position: absolute;
				top: 0;
				right: 0;
			}

			.btn {
				opacity: 0.16;
				width: 44px;
				height: 44px;
				display: flex;
				-webkit-user-select: none;
				-moz-user-select: none;
				-ms-user-select: none;
				user-select: none;
				cursor: default;
				transition: opacity 0.3s;
			}

			.btn--bright {
				opacity: 0.5;
			}

			@media (min-width: 800px) {
				.btn:hover {
					opacity: 0.32;
				}

				.btn--bright:hover {
					opacity: 0.75;
				}
			}

			.btn svg {
				display: block;
				margin: auto;
			}
		</style>
	</head>
	<body>
		<!-- partial:index.partial.html -->
		<!-- SVG Spritesheet -->
		<div style="height: 0; width: 0; position: absolute; visibility: hidden;">
			<svg xmlns="http://www.w3.org/2000/svg">
				<symbol id="icon-play" viewBox="0 0 24 24">
					<path d="M8 5v14l11-7z" />
				</symbol>
				<symbol id="icon-pause" viewBox="0 0 24 24">
					<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
				</symbol>
				<symbol id="icon-close" viewBox="0 0 24 24">
					<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" />
				</symbol>
				<symbol id="icon-settings" viewBox="0 0 24 24">
					<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" />
				</symbol>
				<symbol id="icon-shutter-fast" viewBox="0 0 24 24">
					<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" />
				</symbol>
				<symbol id="icon-shutter-slow" viewBox="0 0 24 24">
					<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" />
				</symbol>
			</svg>
		</div>

		<!-- App -->
		<div class="container">
			<div id="loading-init">Loading...</div>
			<div id="stage-container" class="remove">
				<div id="canvas-container">
					<canvas id="trails-canvas"></canvas>
					<canvas id="main-canvas"></canvas>
				</div>
				<div id="controls">
					<div id="pause-btn" class="btn">
						<svg fill="white" width="24" height="24">
							<use href="#icon-pause"></use>
						</svg>
					</div>
					<div id="shutter-btn" class="btn">
						<svg fill="white" width="24" height="24">
							<use href="#icon-shutter-slow"></use>
						</svg>
					</div>
					<div id="settings-btn" class="btn">
						<svg fill="white" width="24" height="24">
							<use href="#icon-settings"></use>
						</svg>
					</div>
				</div>
				<div id="menu" class="hide">
					<div id="close-menu-btn" class="btn btn--bright">
						<svg fill="white" width="24" height="24">
							<use href="#icon-close"></use>
						</svg>
					</div>
					<div id="menu__header">Settings</div>
					<form>
						<div class="form-option form-option--select">
							<label>Shell Type</label>
							<select id="shell-type"></select>
						</div>
						<div class="form-option form-option--select">
							<label>Shell Size</label>
							<select id="shell-size"></select>
						</div>
						<div class="form-option form-option--checkbox">
							<label id="auto-launch-label"><input id="auto-launch" type="checkbox" /><span>Auto
									Fire</span></label>
						</div>
						<div class="form-option form-option--checkbox">
							<label id="finale-mode-label"><input id="finale-mode" type="checkbox" /><span>Finale
									Mode</span></label>
						</div>
						<div class="form-option form-option--checkbox">
							<label id="hide-controls-label"><input id="hide-controls" type="checkbox" /><span>Hide
									Controls</span></label>
						</div>
					</form>
				</div>
			</div>
		</div>
		<!-- partial -->
		<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/fscreen%401.0.1.js'></script>
		<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/Stage%400.1.4.js'></script>
		<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/329180/MyMath.js'></script>
		<script>
			'use strict';
			console.clear();


			const IS_MOBILE = window.innerWidth <= 640;
			const IS_DESKTOP = window.innerWidth > 800;
			const IS_HEADER = IS_DESKTOP && window.innerHeight < 300;
			// 8K - can restrict this if needed
			const MAX_WIDTH = 7680;
			const MAX_HEIGHT = 4320;
			const GRAVITY = 0.9; // Acceleration in px/s
			let simSpeed = 1;

			const COLOR = {
				Red: '#ff0043',
				Green: '#14fc56',
				Blue: '#1e7fff',
				Purple: '#e60aff',
				Gold: '#ffae00',
				White: '#ffffff'
			};

			// Special invisible color (not rendered, and therefore not in COLOR map)
			const INVISIBLE = '_INVISIBLE_';


			// Interactive state management
			const store = {
				_listeners: new Set(),
				_dispatch() {
					this._listeners.forEach(listener => listener(this.state))
				},
				state: {
					paused: false,
					longExposure: false,
					menuOpen: false,
					config: {
						shell: 'Random',
						size: IS_DESKTOP && !IS_HEADER ? '3' : '1',
						autoLaunch: true,
						finale: false,
						hideControls: IS_HEADER
					}
				},
				setState(nextState) {
					this.state = Object.assign({}, this.state, nextState);
					this._dispatch();
					this.persist();
				},
				subscribe(listener) {
					this._listeners.add(listener);
					return () => this._listeners.remove(listener);
				},
				// Load / persist select state to localStorage
				load() {
					if (localStorage.getItem('schemaVersion') === '1') {
						this.state.config.size = JSON.parse(localStorage.getItem('configSize'));
						this.state.config.hideControls = JSON.parse(localStorage.getItem('hideControls'));
					}
				},
				persist() {
					localStorage.setItem('schemaVersion', '1');
					localStorage.setItem('configSize', JSON.stringify(this.state.config.size));
					localStorage.setItem('hideControls', JSON.stringify(this.state.config.hideControls));
				}
			};

			if (!IS_HEADER) {
				store.load();
			}

			// Actions
			// ---------

			function togglePause(toggle) {
				if (typeof toggle === 'boolean') {
					store.setState({
						paused: toggle
					});
				} else {
					store.setState({
						paused: !store.state.paused
					});
				}
			}

			function toggleLongExposure(toggle) {
				if (typeof toggle === 'boolean') {
					store.setState({
						longExposure: toggle
					});
				} else {
					store.setState({
						longExposure: !store.state.longExposure
					});
				}
			}

			function toggleMenu(toggle) {
				if (typeof toggle === 'boolean') {
					store.setState({
						menuOpen: toggle
					});
				} else {
					store.setState({
						menuOpen: !store.state.menuOpen
					});
				}
			}

			function updateConfig(nextConfig) {
				nextConfig = nextConfig || getConfigFromDOM();
				store.setState({
					config: Object.assign({}, store.state.config, nextConfig)
				});
			}

			// Selectors
			// -----------

			const canInteract = () => !store.state.paused && !store.state.menuOpen;
			const shellNameSelector = () => store.state.config.shell;
			// Converts shell size to number.
			const shellSizeSelector = () => +store.state.config.size;
			const finaleSelector = () => store.state.config.finale;


			// Render app UI / keep in sync with state
			const appNodes = {
				stageContainer: '#stage-container',
				canvasContainer: '#canvas-container',
				controls: '#controls',
				menu: '#menu',
				pauseBtn: '#pause-btn',
				pauseBtnSVG: '#pause-btn use',
				shutterBtn: '#shutter-btn',
				shutterBtnSVG: '#shutter-btn use',
				shellType: '#shell-type',
				shellSize: '#shell-size',
				autoLaunch: '#auto-launch',
				autoLaunchLabel: '#auto-launch-label',
				finaleMode: '#finale-mode',
				finaleModeLabel: '#finale-mode-label',
				hideControls: '#hide-controls',
				hideControlsLabel: '#hide-controls-label'
			};

			// Convert appNodes selectors to dom nodes
			Object.keys(appNodes).forEach(key => {
				appNodes[key] = document.querySelector(appNodes[key]);
			});

			// Remove loading state
			document.getElementById('loading-init').remove();
			appNodes.stageContainer.classList.remove('remove');

			// First render is called in init()
			function renderApp(state) {
				appNodes.pauseBtnSVG.setAttribute('href', `#icon-${state.paused ? 'play' : 'pause'}`);
				appNodes.shutterBtnSVG.setAttribute('href', `#icon-shutter-${state.longExposure ? 'fast' : 'slow'}`);
				appNodes.controls.classList.toggle('hide', state.menuOpen || state.config.hideControls);
				appNodes.canvasContainer.classList.toggle('blur', state.menuOpen);
				appNodes.menu.classList.toggle('hide', !state.menuOpen);
				appNodes.finaleModeLabel.style.opacity = state.config.autoLaunch ? 1 : 0.32;
				appNodes.shellType.value = state.config.shell;
				appNodes.shellSize.value = state.config.size;
				appNodes.autoLaunch.checked = state.config.autoLaunch;
				appNodes.finaleMode.checked = state.config.finale;
				appNodes.hideControls.checked = state.config.hideControls;
			}

			store.subscribe(renderApp);


			function getConfigFromDOM() {
				return {
					shell: appNodes.shellType.value,
					size: appNodes.shellSize.value,
					autoLaunch: appNodes.autoLaunch.checked,
					finale: appNodes.finaleMode.checked,
					hideControls: appNodes.hideControls.checked
				};
			};

			const updateConfigNoEvent = () => updateConfig();
			appNodes.shellType.addEventListener('input', updateConfigNoEvent);
			appNodes.shellSize.addEventListener('input', updateConfigNoEvent);
			appNodes.autoLaunchLabel.addEventListener('click', () => setTimeout(updateConfig, 0));
			appNodes.finaleModeLabel.addEventListener('click', () => setTimeout(updateConfig, 0));
			appNodes.hideControlsLabel.addEventListener('click', () => setTimeout(updateConfig, 0));


			// Constant derivations
			const COLOR_NAMES = Object.keys(COLOR);
			const COLOR_CODES = COLOR_NAMES.map(colorName => COLOR[colorName]);
			// Invisible stars need an indentifier, even through they won't be rendered - physics still apply.
			const COLOR_CODES_W_INVIS = [...COLOR_CODES, INVISIBLE];
			// Tuples is a map keys by color codes (hex) with values of { r, g, b } tuples (still just objects).
			const COLOR_TUPLES = {};
			COLOR_CODES.forEach(hex => {
				COLOR_TUPLES[hex] = {
					r: parseInt(hex.substr(1, 2), 16),
					g: parseInt(hex.substr(3, 2), 16),
					b: parseInt(hex.substr(5, 2), 16),
				};
			});

			// Get a random color.
			function randomColorSimple() {
				return COLOR_CODES[Math.random() * COLOR_CODES.length | 0];
			}

			// Get a random color, with some customization options available.
			let lastColor;

			function randomColor(options) {
				const notSame = options && options.notSame;
				const notColor = options && options.notColor;
				const limitWhite = options && options.limitWhite;
				let color = randomColorSimple();
				// limit the amount of white chosen randomly
				if (limitWhite && color === COLOR.White && Math.random() < 0.6) {
					color = randomColorSimple();
				}
				if (notSame) {
					while (color === lastColor) {
						color = randomColorSimple();
					}
				} else if (notColor) {
					while (color === notColor) {
						color = randomColorSimple();
					}
				}
				lastColor = color;
				return color;
			}

			function whiteOrGold() {
				return Math.random() < 0.5 ? COLOR.Gold : COLOR.White;
			}

			const PI_2 = Math.PI * 2;
			const PI_HALF = Math.PI * 0.5;

			const trailsStage = new Stage('trails-canvas');
			const mainStage = new Stage('main-canvas');
			const stages = [
				trailsStage,
				mainStage
			];

			// Fill trails canvas with black to start.
			trailsStage.ctx.fillStyle = '#000';
			trailsStage.ctx.fillRect(0, 0, trailsStage.width, trailsStage.height);


			// Fullscreen helpers, using Fscreen for prefixes
			function requestFullscreen() {
				if (fullscreenEnabled() && !isFullscreen()) {
					fscreen.requestFullscreen(document.documentElement);
				}
			}

			function fullscreenEnabled() {
				return fscreen.fullscreenEnabled;
			}

			function isFullscreen() {
				return !!fscreen.fullscreenElement;
			}


			// Shell helpers
			function makePistilColor(shellColor) {
				return (shellColor === COLOR.White || shellColor === COLOR.Gold) ? randomColor({
					notColor: shellColor
				}) : whiteOrGold();
			}

			// Unique shell types
			const crysanthemumShell = (size = 1) => {
				const glitter = Math.random() < 0.25;
				const singleColor = Math.random() < 0.68;
				const color = singleColor ? randomColor({
					limitWhite: true
				}) : [randomColor(), randomColor({
					notSame: true
				})];
				const pistil = singleColor && Math.random() < 0.42;
				const pistilColor = makePistilColor(color);
				const streamers = !pistil && color !== COLOR.White && Math.random() < 0.42;
				return {
					size: 300 + size * 100,
					starLife: 900 + size * 200,
					starDensity: glitter ? 1.1 : 1.5,
					color,
					glitter: glitter ? 'light' : '',
					glitterColor: whiteOrGold(),
					pistil,
					pistilColor,
					streamers
				};
			};


			const palmShell = (size = 1) => ({
				size: 250 + size * 75,
				starDensity: 0.6,
				starLife: 1800 + size * 200,
				glitter: 'heavy'
			});

			const ringShell = (size = 1) => {
				const color = randomColor();
				const pistil = Math.random() < 0.75;
				return {
					ring: true,
					color,
					size: 300 + size * 100,
					starLife: 900 + size * 200,
					starCount: 2.2 * PI_2 * (size + 1),
					pistil,
					pistilColor: makePistilColor(color),
					glitter: !pistil ? 'light' : '',
					glitterColor: color === COLOR.Gold ? COLOR.Gold : COLOR.White
				};
			};

			const crossetteShell = (size = 1) => {
				const color = randomColor({
					limitWhite: true
				});
				return {
					size: 300 + size * 100,
					starLife: 900 + size * 200,
					starLifeVariation: 0.22,
					color,
					crossette: true,
					pistil: Math.random() < 0.5,
					pistilColor: makePistilColor(color)
				};
			};

			const floralShell = (size = 1) => ({
				size: 300 + size * 120,
				starDensity: 0.38,
				starLife: 500 + size * 50,
				starLifeVariation: 0.5,
				color: Math.random() < 0.65 ? 'random' : (Math.random() < 0.15 ? randomColor() : [randomColor(),
					randomColor({
						notSame: true
					})
				]),
				floral: true
			});

			const fallingLeavesShell = (size = 1) => ({
				color: INVISIBLE,
				size: 300 + size * 120,
				starDensity: 0.38,
				starLife: 500 + size * 50,
				starLifeVariation: 0.5,
				glitter: 'medium',
				glitterColor: COLOR.Gold,
				fallingLeaves: true
			});

			const willowShell = (size = 1) => ({
				size: 300 + size * 100,
				starDensity: 0.7,
				starLife: 3000 + size * 300,
				glitter: 'willow',
				glitterColor: COLOR.Gold,
				color: INVISIBLE
			});

			const crackleShell = (size = 1) => {
				// favor gold
				const color = Math.random() < 0.75 ? COLOR.Gold : randomColor();
				return {
					size: 380 + size * 75,
					starDensity: 1,
					starLife: 600 + size * 100,
					starLifeVariation: 0.32,
					glitter: 'light',
					glitterColor: COLOR.Gold,
					color,
					crackle: true,
					pistil: Math.random() < 0.65,
					pistilColor: makePistilColor(color)
				};
			};

			const horsetailShell = (size = 1) => {
				const color = randomColor();
				return {
					horsetail: true,
					color,
					size: 250 + size * 38,
					starDensity: 0.85 + size * 0.1,
					starLife: 2500 + size * 300,
					glitter: 'medium',
					glitterColor: Math.random() < 0.5 ? whiteOrGold() : color
				};
			};

			function randomShellName() {
				return Math.random() < 0.6 ? 'Crysanthemum' : shellNames[(Math.random() * (shellNames.length - 1) + 1) | 0];
			}

			function randomShell(size) {
				return shellTypes[randomShellName()](size);
			}

			function shellFromConfig(size) {
				return shellTypes[shellNameSelector()](size);
			}

			// Get a random shell, not including processing intensive varients
			// Note this is only random when "Random" shell is selected in config.
			// Also, this does not create the shell, only returns the factory function.
			const fastShellBlacklist = ['Falling Leaves', 'Floral', 'Willow'];

			function randomFastShell() {
				const isRandom = shellNameSelector() === 'Random';
				let shellName = isRandom ? randomShellName() : shellNameSelector();
				if (isRandom) {
					while (fastShellBlacklist.includes(shellName)) {
						shellName = randomShellName();
					}
				}
				return shellTypes[shellName];
			}


			const shellTypes = {
				'Random': randomShell,
				'Crackle': crackleShell,
				'Crossette': crossetteShell,
				'Crysanthemum': crysanthemumShell,
				'Falling Leaves': fallingLeavesShell,
				'Floral': floralShell,
				'Horse Tail': horsetailShell,
				'Palm': palmShell,
				'Ring': ringShell,
				'Willow': willowShell
			};

			const shellNames = Object.keys(shellTypes);

			function init() {
				// Populate dropdowns
				// shell type
				let options = '';
				shellNames.forEach(opt => options += `<option value="${opt}">${opt}</option>`);
				appNodes.shellType.innerHTML = options;
				// shell size
				options = '';
				['3"', '5"', '6"', '8"', '12"'].forEach((opt, i) => options += `<option value="${i}">${opt}</option>`);
				appNodes.shellSize.innerHTML = options;
				// initial render
				renderApp(store.state);
			}


			function fitShellPositionInBoundsH(position) {
				const edge = 0.18;
				return (1 - edge * 2) * position + edge;
			}

			function fitShellPositionInBoundsV(position) {
				return position * 0.75;
			}

			function getRandomShellPositionH() {
				return fitShellPositionInBoundsH(Math.random());
			}

			function getRandomShellPositionV() {
				return fitShellPositionInBoundsV(Math.random());
			}

			function getRandomShellSize() {
				const baseSize = shellSizeSelector();
				const maxVariance = Math.min(2.5, baseSize);
				const variance = Math.random() * maxVariance;
				const size = baseSize - variance;
				const height = maxVariance === 0 ? Math.random() : 1 - (variance / maxVariance);
				const centerOffset = Math.random() * (1 - height * 0.65) * 0.5;
				const x = Math.random() < 0.5 ? 0.5 - centerOffset : 0.5 + centerOffset;
				return {
					size,
					x: fitShellPositionInBoundsH(x),
					height: fitShellPositionInBoundsV(height)
				};
			}


			// Launches a shell from a user pointer event, based on state.config
			function launchShellFromConfig(event) {
				const shell = new Shell(shellFromConfig(shellSizeSelector()));
				const w = mainStage.width;
				const h = mainStage.height;
				shell.launch(
					event ? event.x / w : getRandomShellPositionH(),
					event ? 1 - event.y / h : getRandomShellPositionV()
				);
			}


			// Sequences
			// -----------

			function seqRandomShell() {
				const size = getRandomShellSize();
				const shell = new Shell(shellFromConfig(size.size));
				shell.launch(size.x, size.height);
				let extraDelay = shell.starLife;
				if (shell.fallingLeaves) {
					extraDelay = 4000;
				}
				return 900 + Math.random() * 600 + extraDelay;
			}

			function seqTwoRandom() {
				const size1 = getRandomShellSize();
				const size2 = getRandomShellSize();
				const shell1 = new Shell(shellFromConfig(size1.size));
				const shell2 = new Shell(shellFromConfig(size2.size));
				const leftOffset = Math.random() * 0.2 - 0.1;
				const rightOffset = Math.random() * 0.2 - 0.1;
				shell1.launch(0.3 + leftOffset, size1.height);
				shell2.launch(0.7 + rightOffset, size2.height);
				let extraDelay = Math.max(shell1.starLife, shell2.starLife);
				if (shell1.fallingLeaves || shell2.fallingLeaves) {
					extraDelay = 4000;
				}
				return 900 + Math.random() * 600 + extraDelay;
			}

			function seqTriple() {
				const shellType = randomFastShell();
				const baseSize = shellSizeSelector();
				const smallSize = Math.max(0, baseSize - 1.25);
				const offset = Math.random() * 0.08 - 0.04;
				const shell1 = new Shell(shellType(baseSize));
				shell1.launch(0.5 + offset, 0.7);
				const leftDelay = 1000 + Math.random() * 400;
				const rightDelay = 1000 + Math.random() * 400;
				setTimeout(() => {
					const offset = Math.random() * 0.08 - 0.04;
					const shell2 = new Shell(shellType(smallSize));
					shell2.launch(0.2 + offset, 0.1);
				}, leftDelay);
				setTimeout(() => {
					const offset = Math.random() * 0.08 - 0.04;
					const shell3 = new Shell(shellType(smallSize));
					shell3.launch(0.8 + offset, 0.1);
				}, rightDelay);
				return 4000;
			}

			function seqSmallBarrage() {
				seqSmallBarrage.lastCalled = Date.now();
				const barrageCount = IS_DESKTOP ? 11 : 5;
				const shellSize = Math.max(0, shellSizeSelector() - 2);
				const useCrysanthemum = Math.random() < 0.7;
				// (cos(x*5π+0.5π)+1)/2 is a custom wave bounded by 0 and 1 used to set varying launch heights
				function launchShell(x) {
					const isRandom = shellNameSelector() === 'Random';
					let shellType = isRandom ? (useCrysanthemum ? crysanthemumShell : randomFastShell()) : shellTypes[
						shellNameSelector()];
					const shell = new Shell(shellType(shellSize));
					const height = (Math.cos(x * 5 * Math.PI + PI_HALF) + 1) / 2;
					shell.launch(x, height * 0.75);
				}
				let count = 0;
				let delay = 0;
				while (count < barrageCount) {
					if (count === 0) {
						launchShell(0.5)
						count += 1;
					} else {
						const offset = (count + 1) / barrageCount / 2;
						setTimeout(() => {
							launchShell(0.5 + offset);
							launchShell(0.5 - offset);
						}, delay);
						count += 2;
					}
					delay += 200;
				}
				return 3400 + barrageCount * 120;
			}
			seqSmallBarrage.cooldown = 15000;
			seqSmallBarrage.lastCalled = Date.now();


			const sequences = [
				seqRandomShell,
				seqTwoRandom,
				seqTriple,
				seqSmallBarrage
			];


			let isFirstSeq = true;
			const finaleCount = 32;
			let currentFinaleCount = 0;

			function startSequence() {
				if (isFirstSeq) {
					isFirstSeq = false;
					const shell = new Shell(crysanthemumShell(shellSizeSelector()));
					shell.launch(0.5, 0.5);
					return 2400;
				}
				if (finaleSelector()) {
					seqRandomShell();
					if (currentFinaleCount < finaleCount) {
						currentFinaleCount++;
						return 170;
					} else {
						currentFinaleCount = 0;
						return 6000;
					}
				}
				const rand = Math.random();
				if (rand < 0.2 && Date.now() - seqSmallBarrage.lastCalled > seqSmallBarrage.cooldown) {
					return seqSmallBarrage();
				}
				if (rand < 0.6) {
					return seqRandomShell();
				} else if (rand < 0.8) {
					return seqTwoRandom();
				} else if (rand < 1) {
					return seqTriple();
				}
			}


			let activePointerCount = 0;
			let isUpdatingSpeed = false;

			function handlePointerStart(event) {
				activePointerCount++;
				const btnSize = 44;
				if (event.y < btnSize) {
					if (event.x < btnSize) {
						togglePause();
						return;
					}
					if (event.x > mainStage.width / 2 - btnSize / 2 && event.x < mainStage.width / 2 + btnSize / 2) {
						toggleLongExposure();
						return;
					}
					if (event.x > mainStage.width - btnSize) {
						toggleMenu();
						return;
					}
				}
				if (!canInteract()) return;
				if (updateSpeedFromEvent(event)) {
					isUpdatingSpeed = true;
				} else if (event.onCanvas) {
					launchShellFromConfig(event);
				}
			}

			function handlePointerEnd(event) {
				activePointerCount--;
				isUpdatingSpeed = false;
			}

			function handlePointerMove(event) {
				if (!canInteract()) return;
				if (isUpdatingSpeed) {
					updateSpeedFromEvent(event);
				}
			}

			function handleKeydown(event) {
				// P
				if (event.keyCode === 80) {
					togglePause();
				}
				// O
				else if (event.keyCode === 79) {
					toggleMenu();
				}
				// Esc
				else if (event.keyCode === 27) {
					toggleMenu(false);
				}
			}

			mainStage.addEventListener('pointerstart', handlePointerStart);
			mainStage.addEventListener('pointerend', handlePointerEnd);
			mainStage.addEventListener('pointermove', handlePointerMove);
			window.addEventListener('keydown', handleKeydown);
			// Try to go fullscreen upon a touch
			window.addEventListener('touchend', (event) => !IS_DESKTOP && requestFullscreen());


			function handleResize() {
				const w = window.innerWidth;
				const h = window.innerHeight;
				// Try to adopt screen size, heeding maximum sizes specified
				const containerW = Math.min(w, MAX_WIDTH);
				// On small screens, use full device height
				const containerH = w <= 420 ? h : Math.min(h, MAX_HEIGHT);
				appNodes.stageContainer.style.width = containerW + 'px';
				appNodes.stageContainer.style.height = containerH + 'px';
				stages.forEach(stage => stage.resize(containerW, containerH));
			}

			// Compute initial dimensions
			handleResize();

			window.addEventListener('resize', handleResize);


			// Dynamic globals
			let speedBarOpacity = 0;
			let autoLaunchTime = 0;

			function updateSpeedFromEvent(event) {
				if (isUpdatingSpeed || event.y >= mainStage.height - 44) {
					// 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.
					const edge = 16;
					const newSpeed = (event.x - edge) / (mainStage.width - edge * 2);
					simSpeed = Math.min(Math.max(newSpeed, 0), 1);
					// show speed bar after an update
					speedBarOpacity = 1;
					// If we updated the speed, return true
					return true;
				}
				// Return false if the speed wasn't updated
				return false;
			}


			// Extracted function to keep `update()` optimized
			function updateGlobals(timeStep, lag) {
				// Always try to fade out speed bar
				if (!isUpdatingSpeed) {
					speedBarOpacity -= lag / 30; // half a second
					if (speedBarOpacity < 0) {
						speedBarOpacity = 0;
					}
				}
				// auto launch shells
				if (store.state.config.autoLaunch) {
					autoLaunchTime -= timeStep;
					if (autoLaunchTime <= 0) {
						autoLaunchTime = startSequence();
					}
				}
			}


			function update(frameTime, lag) {
				if (!canInteract()) return;
				const {
					width,
					height
				} = mainStage;
				const timeStep = frameTime * simSpeed;
				const speed = simSpeed * lag;
				updateGlobals(timeStep, lag);
				const starDrag = 1 - (1 - Star.airDrag) * speed;
				const starDragHeavy = 1 - (1 - Star.airDragHeavy) * speed;
				const sparkDrag = 1 - (1 - Spark.airDrag) * speed;
				const gAcc = timeStep / 1000 * GRAVITY;
				COLOR_CODES_W_INVIS.forEach(color => {
					// Stars
					Star.active[color].forEach((star, i, stars) => {
						star.life -= timeStep;
						if (star.life <= 0) {
							stars.splice(i, 1);
							Star.returnInstance(star);
						} else {
							star.prevX = star.x;
							star.prevY = star.y;
							star.x += star.speedX * speed;
							star.y += star.speedY * speed;
							// Apply air drag if star isn't "heavy". The heavy property is used for the shell comets.
							if (!star.heavy) {
								star.speedX *= starDrag;
								star.speedY *= starDrag;
							} else {
								star.speedX *= starDragHeavy;
								star.speedY *= starDragHeavy;
							}
							star.speedY += gAcc;
							if (star.spinRadius) {
								star.spinAngle += star.spinSpeed * speed;
								star.x += Math.sin(star.spinAngle) * star.spinRadius * speed;
								star.y += Math.cos(star.spinAngle) * star.spinRadius * speed;
							}
							if (star.sparkFreq) {
								star.sparkTimer -= timeStep;
								while (star.sparkTimer < 0) {
									star.sparkTimer += star.sparkFreq;
									Spark.add(
										star.x,
										star.y,
										star.sparkColor,
										Math.random() * PI_2,
										Math.random() * star.sparkSpeed,
										star.sparkLife * 0.8 + Math.random() * star.sparkLifeVariation * star
										.sparkLife
									);
								}
							}
						}
					});
					// Sparks
					Spark.active[color].forEach((spark, i, sparks) => {
						spark.life -= timeStep;
						if (spark.life <= 0) {
							sparks.splice(i, 1);
							Spark.returnInstance(spark);
						} else {
							spark.prevX = spark.x;
							spark.prevY = spark.y;
							spark.x += spark.speedX * speed;
							spark.y += spark.speedY * speed;
							spark.speedX *= sparkDrag;
							spark.speedY *= sparkDrag;
							spark.speedY += gAcc;
						}
					});
				});
				render(speed);
			}

			function render(speed) {
				const {
					dpr,
					width,
					height
				} = mainStage;
				const trailsCtx = trailsStage.ctx;
				const mainCtx = mainStage.ctx;
				colorSky(speed);
				trailsCtx.scale(dpr, dpr);
				mainCtx.scale(dpr, dpr);
				trailsCtx.globalCompositeOperation = 'source-over';
				trailsCtx.fillStyle = `rgba(0, 0, 0, ${store.state.longExposure ? 0.0025 : 0.1 * speed})`;
				trailsCtx.fillRect(0, 0, width, height);
				// Remaining drawing on trails canvas will use 'lighten' blend mode
				trailsCtx.globalCompositeOperation = 'lighten';
				mainCtx.clearRect(0, 0, width, height);
				// Draw queued burst flashes
				while (BurstFlash.active.length) {
					const bf = BurstFlash.active.pop();
					const burstGradient = trailsCtx.createRadialGradient(bf.x, bf.y, 0, bf.x, bf.y, bf.radius);
					burstGradient.addColorStop(0.05, 'white');
					burstGradient.addColorStop(0.25, 'rgba(255, 160, 20, 0.2)');
					burstGradient.addColorStop(1, 'rgba(255, 160, 20, 0)');
					trailsCtx.fillStyle = burstGradient;
					trailsCtx.fillRect(bf.x - bf.radius, bf.y - bf.radius, bf.radius * 2, bf.radius * 2);
					BurstFlash.returnInstance(bf);
				}
				// Draw stars
				trailsCtx.lineWidth = Star.drawWidth;
				trailsCtx.lineCap = 'round';
				mainCtx.strokeStyle = '#fff';
				mainCtx.lineWidth = 1;
				mainCtx.beginPath();
				COLOR_CODES.forEach(color => {
					const stars = Star.active[color];
					trailsCtx.strokeStyle = color;
					trailsCtx.beginPath();
					stars.forEach(star => {
						trailsCtx.moveTo(star.x, star.y);
						trailsCtx.lineTo(star.prevX, star.prevY);
						mainCtx.moveTo(star.x, star.y);
						mainCtx.lineTo(star.x - star.speedX * 1.6, star.y - star.speedY * 1.6);
					});
					trailsCtx.stroke();
				});
				mainCtx.stroke();

				// Draw sparks
				trailsCtx.lineWidth = Spark.drawWidth;
				trailsCtx.lineCap = 'butt';
				COLOR_CODES.forEach(color => {
					const sparks = Spark.active[color];
					trailsCtx.strokeStyle = color;
					trailsCtx.beginPath();
					sparks.forEach(spark => {
						trailsCtx.moveTo(spark.x, spark.y);
						trailsCtx.lineTo(spark.prevX, spark.prevY);
					});
					trailsCtx.stroke();
				});
				// Render speed bar if visible
				if (speedBarOpacity) {
					const speedBarHeight = 6;
					mainCtx.globalAlpha = speedBarOpacity;
					mainCtx.fillStyle = COLOR.Blue;
					mainCtx.fillRect(0, height - speedBarHeight, width * simSpeed, speedBarHeight);
					mainCtx.globalAlpha = 1;
				}
				trailsCtx.resetTransform();
				mainCtx.resetTransform();
			}


			// Draw colored overlay based on combined brightness of stars (light up the sky!)
			// Note: this is applied to the canvas container's background-color, so it's behind the particles
			const currentSkyColor = {
				r: 0,
				g: 0,
				b: 0
			};
			const targetSkyColor = {
				r: 0,
				g: 0,
				b: 0
			};

			function colorSky(speed) {
				// The maximum r, g, or b value that will be used (255 would represent no maximum)
				const maxSkySaturation = 30;
				// How many stars are required in total to reach maximum sky brightness
				const maxStarCount = 500;
				let totalStarCount = 0;
				// Initialize sky as black
				targetSkyColor.r = 0;
				targetSkyColor.g = 0;
				targetSkyColor.b = 0;
				// 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.
				// Also add up total star count.
				COLOR_CODES.forEach(color => {
					const tuple = COLOR_TUPLES[color];
					const count = Star.active[color].length;
					totalStarCount += count;
					targetSkyColor.r += tuple.r * count;
					targetSkyColor.g += tuple.g * count;
					targetSkyColor.b += tuple.b * count;
				});
				// 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.
				const intensity = Math.pow(Math.min(1, totalStarCount / maxStarCount), 0.3);
				// Figure out which color component has the highest value, so we can scale them without affecting the ratios.
				// Prevent 0 from being used, so we don't divide by zero in the next step.
				const maxColorComponent = Math.max(1, targetSkyColor.r, targetSkyColor.g, targetSkyColor.b);
				// Scale all color components to a max of `maxSkySaturation`, and apply intensity.
				targetSkyColor.r = targetSkyColor.r / maxColorComponent * maxSkySaturation * intensity;
				targetSkyColor.g = targetSkyColor.g / maxColorComponent * maxSkySaturation * intensity;
				targetSkyColor.b = targetSkyColor.b / maxColorComponent * maxSkySaturation * intensity;
				// Animate changes to color to smooth out transitions.
				const colorChange = 10;
				currentSkyColor.r += (targetSkyColor.r - currentSkyColor.r) / colorChange * speed;
				currentSkyColor.g += (targetSkyColor.g - currentSkyColor.g) / colorChange * speed;
				currentSkyColor.b += (targetSkyColor.b - currentSkyColor.b) / colorChange * speed;
				appNodes.canvasContainer.style.backgroundColor =
					`rgb(${currentSkyColor.r | 0}, ${currentSkyColor.g | 0}, ${currentSkyColor.b | 0})`;
			}

			mainStage.addEventListener('ticker', update);


			// Helper used to semi-randomly spread particles over an arc
			// Values are flexible - `start` and `arcLength` can be negative, and `randomness` is simply a multiplier for random addition.
			function createParticleArc(start, arcLength, count, randomness, particleFactory) {
				const angleDelta = arcLength / count;
				// Sometimes there is an extra particle at the end, too close to the start. Subtracting half the angleDelta ensures that is skipped.
				// Would be nice to fix this a better way.
				const end = start + arcLength - (angleDelta * 0.5);
				if (end > start) {
					// Optimization: `angle=angle+angleDelta` vs. angle+=angleDelta
					// V8 deoptimises with let compound assignment
					for (let angle = start; angle < end; angle = angle + angleDelta) {
						particleFactory(angle + Math.random() * angleDelta * randomness);
					}
				} else {
					for (let angle = start; angle > end; angle = angle + angleDelta) {
						particleFactory(angle + Math.random() * angleDelta * randomness);
					}
				}
			}


			// Various star effects.
			// These are designed to be attached to a star's `onDeath` event.

			// Crossette breaks star into four same-color pieces which branch in a cross-like shape.
			function crossetteEffect(star) {
				const startAngle = Math.random() * PI_HALF;
				createParticleArc(startAngle, PI_2, 4, 0.5, (angle) => {
					Star.add(
						star.x,
						star.y,
						star.color,
						angle,
						Math.random() * 0.6 + 0.75,
						600
					);
				});
			}

			// Flower is like a mini shell
			function floralEffect(star) {
				const startAngle = Math.random() * PI_HALF;
				createParticleArc(startAngle, PI_2, 24, 1, (angle) => {
					Star.add(
						star.x,
						star.y,
						star.color,
						angle,
						// apply near cubic falloff to speed (places more particles towards outside)
						Math.pow(Math.random(), 0.45) * 2.4,
						1000 + Math.random() * 300,
						star.speedX,
						star.speedY
					);
				});
				// Queue burst flash render
				BurstFlash.add(star.x, star.y, 24);
			}

			// Floral burst with willow stars
			function fallingLeavesEffect(star) {
				const startAngle = Math.random() * PI_HALF;
				createParticleArc(startAngle, PI_2, 12, 1, (angle) => {
					const newStar = Star.add(
						star.x,
						star.y,
						INVISIBLE,
						angle,
						// apply near cubic falloff to speed (places more particles towards outside)
						Math.pow(Math.random(), 0.45) * 2.4,
						2400 + Math.random() * 600,
						star.speedX,
						star.speedY
					);
					newStar.sparkColor = COLOR.Gold;
					newStar.sparkFreq = 72;
					newStar.sparkSpeed = 0.28;
					newStar.sparkLife = 750;
					newStar.sparkLifeVariation = 3.2;
				});
				// Queue burst flash render
				BurstFlash.add(star.x, star.y, 24);
			}

			// Crackle pops into a small cloud of golden sparks.
			function crackleEffect(star) {
				createParticleArc(0, PI_2, 10, 1.8, (angle) => {
					Spark.add(
						star.x,
						star.y,
						COLOR.Gold,
						angle,
						// apply near cubic falloff to speed (places more particles towards outside)
						Math.pow(Math.random(), 0.45) * 2.4,
						300 + Math.random() * 200
					);
				});
			}



			/**
			 * Shell can be constructed with options:
			 *
			 * size:      Size of the burst.
			 * starCount: Number of stars to create. This is optional, and will be set to a reasonable quantity for size if omitted.
			 * starLife:
			 * starLifeVariation:
			 * color:
			 * glitterColor:
			 * glitter: One of: 'light', 'medium', 'heavy', 'streamer', 'willow'
			 * pistil:
			 * pistilColor:
			 * streamers:
			 * crossette:
			 * floral:
			 * crackle:
			 */

			class Shell {
				constructor(options) {
					Object.assign(this, options);
					this.starLifeVariation = options.starLifeVariation || 0.125;
					this.color = options.color || randomColor();
					this.glitterColor = options.glitterColor || this.color;
					// Set default starCount if needed, will be based on shell size and scale exponentially, like a sphere's surface area.
					if (!this.starCount) {
						const density = options.starDensity || 1;
						const scaledSize = this.size / 50 * density;
						this.starCount = scaledSize * scaledSize;
					}
				}
				launch(position, launchHeight) {
					const {
						width,
						height
					} = mainStage;
					// Distance from sides of screen to keep shells.
					const hpad = 60;
					// Distance from top of screen to keep shell bursts.
					const vpad = 50;
					// Minimum burst height, as a percentage of stage height
					const minHeightPercent = 0.45;
					// Minimum burst height in px
					const minHeight = height - height * minHeightPercent;
					const launchX = position * (width - hpad * 2) + hpad;
					const launchY = height;
					const burstY = minHeight - (launchHeight * (minHeight - vpad));
					const launchDistance = launchY - burstY;
					// Using a custom power curve to approximate Vi needed to reach launchDistance under gravity and air drag.
					// Magic numbers came from testing.
					const launchVelocity = Math.pow(launchDistance * 0.04, 0.64);
					const comet = this.comet = Star.add(
						launchX,
						launchY,
						typeof this.color === 'string' && this.color !== 'random' ? this.color : COLOR.White,
						Math.PI,
						launchVelocity * (this.horsetail ? 1.2 : 1),
						// Hang time is derived linearly from Vi; exact number came from testing
						launchVelocity * (this.horsetail ? 100 : 400)
					);
					// making comet "heavy" limits air drag
					comet.heavy = true;
					// comet spark trail
					comet.spinRadius = 0.78;
					comet.sparkFreq = 16;
					if (this.glitter === 'willow' || this.fallingLeaves) {
						comet.sparkFreq = 10;
						comet.sparkSpeed = 0.5;
						comet.sparkLife = 500;
						comet.sparkLifeVariation = 3;
					}
					if (this.color === INVISIBLE) {
						comet.sparkColor = COLOR.Gold;
					}
					comet.onDeath = comet => this.burst(comet.x, comet.y);
					// comet.onDeath = () => this.burst(launchX, burstY);
				}
				burst(x, y) {
					// Set burst speed so overall burst grows to set size. This specific formula was derived from testing, and is affected by simulated air drag.
					const speed = this.size / 96;

					let color, onDeath, sparkFreq, sparkSpeed, sparkLife;
					let sparkLifeVariation = 0.25;
					if (this.crossette) onDeath = crossetteEffect;
					if (this.floral) onDeath = floralEffect;
					if (this.crackle) onDeath = crackleEffect;
					if (this.fallingLeaves) onDeath = fallingLeavesEffect;
					if (this.glitter === 'light') {
						sparkFreq = 200;
						sparkSpeed = 0.25;
						sparkLife = 600;
					} else if (this.glitter === 'medium') {
						sparkFreq = 100;
						sparkSpeed = 0.36;
						sparkLife = 1400;
					} else if (this.glitter === 'heavy') {
						sparkFreq = 42;
						sparkSpeed = 0.62;
						sparkLife = 2800;
					} else if (this.glitter === 'streamer') {
						sparkFreq = 20;
						sparkSpeed = 0.75;
						sparkLife = 800;
					} else if (this.glitter === 'willow') {
						sparkFreq = 72;
						sparkSpeed = 0.28;
						sparkLife = 1000;
						sparkLifeVariation = 3.4;
					}
					const starFactory = angle => {
						const star = Star.add(
							x,
							y,
							color || randomColor(),
							angle,
							// apply near cubic falloff to speed (places more particles towards outside)
							Math.pow(Math.random(), 0.45) * speed,
							// add minor variation to star life
							this.starLife + Math.random() * this.starLife * this.starLifeVariation,
							this.horsetail && this.comet && this.comet.speedX,
							this.horsetail && this.comet && this.comet.speedY
						);

						star.onDeath = onDeath;

						if (this.glitter) {
							star.sparkFreq = sparkFreq;
							star.sparkSpeed = sparkSpeed;
							star.sparkLife = sparkLife;
							star.sparkLifeVariation = sparkLifeVariation;
							star.sparkColor = this.glitterColor;
							star.sparkTimer = Math.random() * star.sparkFreq;
						}
					};
					if (typeof this.color === 'string') {
						if (this.color === 'random') {
							color = null; // falsey value creates random color in starFactory
						} else {
							color = this.color;
						}
						// Rings have positional randomness, but are rotated randomly
						if (this.ring) {
							const ringStartAngle = Math.random() * Math.PI;
							const ringSquash = Math.pow(Math.random(), 0.45) * 0.992 + 0.008;
							createParticleArc(0, PI_2, this.starCount, 0, angle => {
								// Create a ring, squashed horizontally
								const initSpeedX = Math.sin(angle) * speed * ringSquash;
								const initSpeedY = Math.cos(angle) * speed;
								// Rotate ring
								const newSpeed = MyMath.pointDist(0, 0, initSpeedX, initSpeedY);
								const newAngle = MyMath.pointAngle(0, 0, initSpeedX, initSpeedY) + ringStartAngle;
								const star = Star.add(
									x,
									y,
									color,
									newAngle,
									// apply near cubic falloff to speed (places more particles towards outside)
									newSpeed, //speed,
									// add minor variation to star life
									this.starLife + Math.random() * this.starLife * this.starLifeVariation
								);
								if (this.glitter) {
									star.sparkFreq = sparkFreq;
									star.sparkSpeed = sparkSpeed;
									star.sparkLife = sparkLife;
									star.sparkLifeVariation = sparkLifeVariation;
									star.sparkColor = this.glitterColor;
									star.sparkTimer = Math.random() * star.sparkFreq;
								}
							});
						}
						// "Normal burst
						else {
							createParticleArc(0, PI_2, this.starCount, 1, starFactory);
						}
					} else if (Array.isArray(this.color)) {
						let start, start2, arc;
						if (Math.random() < 0.5) {
							start = Math.random() * Math.PI;
							start2 = start + Math.PI;
							arc = Math.PI;
						} else {
							start = 0;
							start2 = 0;
							arc = PI_2;
						}
						color = this.color[0];
						createParticleArc(start, arc, this.starCount / 2, 1, starFactory);
						color = this.color[1];
						createParticleArc(start2, arc, this.starCount / 2, 1, starFactory)
					}
					if (this.pistil) {
						const innerShell = new Shell({
							size: this.size * 0.5,
							starLife: this.starLife * 0.7,
							starLifeVariation: this.starLifeVariation,
							starDensity: 1.65,
							color: this.pistilColor,
							glitter: 'light',
							glitterColor: this.pistilColor === COLOR.Gold ? COLOR.Gold : COLOR.White
						});
						innerShell.burst(x, y);
					}
					if (this.streamers) {
						const innerShell = new Shell({
							size: this.size,
							starLife: this.starLife * 0.8,
							starLifeVariation: this.starLifeVariation,
							starCount: Math.max(6, this.size / 45) | 0,
							color: COLOR.White,
							glitter: 'streamer'
						});
						innerShell.burst(x, y);
					}
					// Queue burst flash render
					BurstFlash.add(x, y, this.size / 8);
				}
			}



			const BurstFlash = {
				active: [],
				_pool: [],
				_new() {
					return {}
				},
				add(x, y, radius) {
					const instance = this._pool.pop() || this._new();
					instance.x = x;
					instance.y = y;
					instance.radius = radius;
					this.active.push(instance);
					return instance;
				},
				returnInstance(instance) {
					this._pool.push(instance);
				}
			};



			// Helper to generate objects for storing active particles.
			// Particles are stored in arrays keyed by color (code, not name) for improved rendering performance.
			function createParticleCollection() {
				const collection = {};
				COLOR_CODES_W_INVIS.forEach(color => {
					collection[color] = [];
				});
				return collection;
			}

			const Star = {
				// Visual properties
				drawWidth: 3,
				airDrag: 0.98,
				airDragHeavy: 0.992,
				// Star particles will be keyed by color
				active: createParticleCollection(),
				_pool: [],
				_new() {
					return {};
				},

				add(x, y, color, angle, speed, life, speedOffX, speedOffY) {
					const instance = this._pool.pop() || this._new();
					instance.heavy = false;
					instance.x = x;
					instance.y = y;
					instance.prevX = x;
					instance.prevY = y;
					instance.color = color;
					instance.speedX = Math.sin(angle) * speed + (speedOffX || 0);
					instance.speedY = Math.cos(angle) * speed + (speedOffY || 0);
					instance.life = life;
					instance.spinAngle = Math.random() * PI_2;
					instance.spinSpeed = 0.8;
					instance.spinRadius = 0;
					instance.sparkFreq = 0; // ms between spark emissions
					instance.sparkSpeed = 1;
					instance.sparkTimer = 0;
					instance.sparkColor = color;
					instance.sparkLife = 750;
					instance.sparkLifeVariation = 0.25;
					this.active[color].push(instance);
					return instance;
				},

				// Public method for cleaning up and returning an instance back to the pool.
				returnInstance(instance) {
					// Call onDeath handler if available (and pass it current star instance)
					instance.onDeath && instance.onDeath(instance);
					// Clean up
					instance.onDeath = null;
					// Add back to the pool.
					this._pool.push(instance);
				}
			};


			const Spark = {
				// Visual properties
				drawWidth: 0.75,
				airDrag: 0.9,
				// Star particles will be keyed by color
				active: createParticleCollection(),
				_pool: [],
				_new() {
					return {};
				},

				add(x, y, color, angle, speed, life) {
					const instance = this._pool.pop() || this._new();
					instance.x = x;
					instance.y = y;
					instance.prevX = x;
					instance.prevY = y;
					instance.color = color;
					instance.speedX = Math.sin(angle) * speed;
					instance.speedY = Math.cos(angle) * speed;
					instance.life = life;
					this.active[color].push(instance);
					return instance;
				},

				// Public method for cleaning up and returning an instance back to the pool.
				returnInstance(instance) {
					// Add back to the pool.
					this._pool.push(instance);
				}
			};



			init();
		</script>
	</body>
</html>
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

信计2102罗铠威

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值