Stagejs实现自带音效可调参数的炫酷烟花绽放动画效果代码

代码语言:html

所属分类:粒子

代码描述:Stagejs实现自带音效可调参数的炫酷烟花绽放动画效果代码

代码标签: Stagej 音效 调参数 炫酷 烟花 绽放

下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <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:.06em
        }
        .hide {
        	opacity:0;
        	visibility:hidden
        }
        .remove {
        	display:none!important
        }
        .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;
        	text-transform:uppercase
        }
        .loading-init__header {
        	font-size:2.2em
        }
        .loading-init__status {
        	margin-top:1em;
        	font-size:.8em;
        	opacity:.75
        }
        .stage-container {
        	overflow:hidden;
        	box-sizing:initial;
        	border:1px solid #222;
        	margin:-1px
        }
        @media(max-width:840px) {
        	.stage-container {
        	border:0;
        	margin:0
        }
        }.canvas-container {
        	width:100%;
        	height:100%;
        	transition:filter .3s
        }
        .canvas-container canvas {
        	position:absolute;
        	mix-blend-mode:lighten;
        	transform:translateZ(0)
        }
        .controls {
        	position:absolute;
        	top:0;
        	width:100%;
        	padding-bottom:50px;
        	display:flex;
        	justify-content:space-between;
        	transition:opacity .3s,visibility .3s
        }
        @media(min-width:840px) {
        	.controls {
        	visibility:visible
        }
        .controls.hide:hover {
        	opacity:1
        }
        }.menu {
        	position:absolute;
        	top:0;
        	bottom:0;
        	left:0;
        	right:0;
        	background-color:rgba(0,0,0,0.42);
        	transition:opacity .3s,visibility .3s
        }
        .menu__inner-wrap {
        	display:flex;
        	flex-direction:column;
        	justify-content:center;
        	align-items:center;
        	position:absolute;
        	top:0;
        	bottom:0;
        	left:0;
        	right:0;
        	transition:opacity .3s
        }
        .menu__header {
        	margin-top:auto;
        	margin-bottom:8px;
        	padding-top:16px;
        	font-size:2em;
        	text-transform:uppercase
        }
        .menu__subheader {
        	margin-bottom:auto;
        	padding-bottom:12px;
        	font-size:.86em;
        	opacity:.8
        }
        .menu form {
        	width:100%;
        	max-width:400px;
        	padding:0 10px;
        	overflow:auto;
        	-webkit-overflow-scrolling:touch
        }
        .menu .form-option {
        	display:flex;
        	align-items:center;
        	margin:16px 0;
        	transition:opacity .3s
        }
        .menu .form-option label {
        	display:block;
        	width:50%;
        	padding-right:12px;
        	text-align:right;
        	text-transform:uppercase;
        	-webkit-user-select:none;
        	-moz-user-select:none;
        	-ms-user-select:none;
        	user-select:none
        }
        .menu .form-option--select select {
        	display:block;
        	width:50%;
        	height:30px;
        	font-size:1rem;
        	font-family:"Russo One",arial,sans-serif;
        	color:rgba(255,255,255,0.5);
        	letter-spacing:.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 input {
        	display:block;
        	width:26px;
        	height:26px;
        	margin:0;
        	opacity:.5
        }
        @media(max-width:840px) {
        	.menu .form-option select,.menu .form-option input {
        	outline:0
        }
        }.close-menu-btn {
        	position:absolute;
        	top:0;
        	right:0
        }
        .btn {
        	opacity:.16;
        	width:50px;
        	height:50px;
        	display:flex;
        	-webkit-user-select:none;
        	-moz-user-select:none;
        	-ms-user-select:none;
        	user-select:none;
        	cursor:default;
        	transition:opacity .3s
        }
        .btn--bright {
        	opacity:.5
        }
        @media(min-width:840px) {
        	.btn:hover {
        	opacity:.32
        }
        .btn--bright:hover {
        	opacity:.75
        }
        }.btn svg {
        	display:block;
        	margin:auto
        }
        .credits {
        	margin-top:auto;
        	margin-bottom:10px;
        	padding-top:6px;
        	font-size:.8em;
        	opacity:.75
        }
        .credits a {
        	color:rgba(255,255,255,0.5);
        	text-decoration:none
        }
        .credits a:hover,.credits a:active {
        	color:rgba(255,255,255,0.75);
        	text-decoration:underline
        }
        .help-modal {
        	display:flex;
        	justify-content:center;
        	align-items:center;
        	position:fixed;
        	top:0;
        	bottom:0;
        	left:0;
        	right:0;
        	visibility:hidden;
        	transition-property:visibility;
        	transition-duration:.25s
        }
        .help-modal__overlay {
        	position:absolute;
        	top:0;
        	bottom:0;
        	left:0;
        	right:0;
        	opacity:0;
        	transition-property:opacity;
        	transition-timing-function:ease-in;
        	transition-duration:.25s
        }
        .help-modal__dialog {
        	display:flex;
        	flex-direction:column;
        	align-items:center;
        	max-width:400px;
        	max-height:calc(100vh - 100px);
        	margin:10px;
        	padding:20px;
        	border-radius:.3em;
        	background-color:rgba(0,0,0,0.4);
        	opacity:0;
        	transform:scale(0.9,0.9);
        	transition-property:opacity,transform;
        	transition-timing-function:ease-in;
        	transition-duration:.25s
        }
        @media(min-width:840px) {
        	.help-modal__dialog {
        	font-size:1.25rem;
        	max-width:500px
        }
        }.help-modal__header {
        	font-size:1.75em;
        	text-transform:uppercase;
        	text-align:center
        }
        .help-modal__body {
        	overflow-y:auto;
        	-webkit-overflow-scrolling:touch;
        	margin:1em 0;
        	padding:1em 0;
        	border-top:1px solid rgba(255,255,255,0.25);
        	border-bottom:1px solid rgba(255,255,255,0.25);
        	line-height:1.5;
        	color:rgba(255,255,255,0.75)
        }
        .help-modal__close-btn {
        	flex-shrink:0;
        	outline:0;
        	border:0;
        	border-radius:2px;
        	padding:.25em .75em;
        	margin-top:.36em;
        	font-family:"Russo One",arial,sans-serif;
        	font-size:1em;
        	color:rgba(255,255,255,0.5);
        	text-transform:uppercase;
        	letter-spacing:.06em;
        	background-color:rgba(255,255,255,0.25);
        	transition:color .3s,background-color .3s
        }
        .help-modal__close-btn:hover,.help-modal__close-btn:active,.help-modal__close-btn:focus {
        	color:#FFF;
        	background-color:#09F
        }
        .help-modal.active {
        	visibility:visible;
        	transition-duration:.4s
        }
        .help-modal.active .help-modal__overlay {
        	opacity:1;
        	transition-timing-function:ease-out;
        	transition-duration:.4s
        }
        .help-modal.active .help-modal__dialog {
        	opacity:1;
        	transform:scale(1,1);
        	transition-timing-function:ease-out;
        	transition-duration:.4s
        }
    </style>
</head>

<body>


    <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-sound-on" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" /></symbol><symbol id="icon-sound-off" viewBox="0 0 24 24"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z" /></symbol></svg></div>
    <!-- App -->
    <div class="container">
        <div class="loading-init">
            <div class="loading-init__header">Loading</div>
            <div class="loading-init__status">Assembling Shells</div>
        </div>
        <div class="stage-container remove">
            <div class="canvas-container"><canvas id="trails-canvas"></canvas><canvas id="main-canvas"></canvas></div>
            <div class="controls">
                <div class="btn pause-btn"><svg fill="white" width="24" height="24"><use href="#icon-pause" xlink:href="#icon-pause"></use></svg></div>
                <div class="btn sound-btn"><svg fill="white" width="24" height="24"><use href="#icon-sound-off" xlink:href="#icon-sound-off"></use></svg></div>
                <div class="btn settings-btn"><svg fill="white" width="24" height="24"><use href="#icon-settings" xlink:href="#icon-settings"></use></svg></div>
            </div>
            <div class="menu hide">
                <div class="menu__inner-wrap">
                    <div class="btn btn--bright close-menu-btn"><svg fill="white" width="24" height="24"><use href="#icon-close" xlink:href="#icon-close"></use></svg></div>
                    <div class="menu__header">Settings</div>
                    <div class="menu__subheader">For more info,click any label.</div>
                    <form>
                        <div class="form-option form-option--select"><label class="shell-type-label">Shell Type</label><select class="shell-type"></select></div>
                        <div class="form-option form-option--select"><label class="shell-size-label">Shell Size</label><select class="shell-size"></select></div>
                        <div class="form-option form-option--select"><label class="quality-ui-label">Quality</label><select class="quality-ui"></select></div>
                        <div class="form-option form-option--select"><label class="sky-lighting-label">Sky Lighting</label><select class="sky-lighting"></select></div>
                        <div class="form-option form-option--select"><label class="scaleFactor-label">Scale</label><select class="scaleFactor"></select></div>
                        <div class="form-option form-option--checkbox"><label class="auto-launch-label">Auto Fire</label><input class="auto-launch" type="checkbox" /></div>
                        <div class="form-option form-option--checkbox form-option--finale-mode"><label class="finale-mode-label">Finale Mode</label><input class="finale-mode" type="checkbox" /></div>
                        <div class="form-option form-option--checkbox"><label class="hide-controls-label">Hide Controls</label><input class="hide-controls" type="checkbox" /></div>
                        <div class="form-option form-option--checkbox form-option--fullscreen"><label class="fullscreen-label">Fullscreen</label><input class="fullscreen" type="checkbox" /></div>
                        <div class="form-option form-option--checkbox"><label class="long-exposure-label">Open Shutter</label><input class="long-exposure" type="checkbox" /></div>
                    </form>
                    <div class="credits">Passionately built by <a href="http://www.bootstrap.com" target="_blank">Caleb Miller</a>. </div>
                </div>
            </div>
        </div>
        <div class="help-modal">
            <div class="help-modal__overlay"></div>
            <div class="help-modal__dialog">
                <div class="help-modal__header"></div>
                <div class="help-modal__body"></div><button type="button" class="help-modal__close-btn">Close</button></div>
        </div>
    </div>
    <!-- partial -->
    <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/fscreen.1.0.1.js"></script>
    <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/Stage.0.1.4.js"></script>
    <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/MyMath.js"></script>
    <script>
        // This is a prime example of what starts out as a simple project
        // and snowballs way beyond its intended size. It's a little clunky
        // reading/working on this single file, but here it is anyways :)
        
        const IS_MOBILE = window.innerWidth <= 640;
        const IS_DESKTOP = window.innerWidth > 800;
        const IS_HEADER = IS_DESKTOP && window.innerHeight < 300;
        // Detect high end devices. This will be a moving target.
        const IS_HIGH_END_DEVICE = (() => {
        	const hwConcurrency = navigator.hardwareConcurrency;
        	if (!hwConcurrency) {
        		return false;
        	}
        	// Large screens indicate a full size computer, which often have hyper threading these days.
        	// So a quad core desktop machine has 8 cores. We'll place a higher min threshold there.
        	const minCount = window.innerWidth <= 1024 ? 4 : 8;
        	return hwConcurrency >= minCount;
        })();
        // Prevent canvases from getting too large on ridiculous screen sizes.
        // 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;
        
        function getDefaultScaleFactor() {
        	if (IS_MOBILE) return 0.9;
        	if (IS_HEADER) return 0.75;
        	return 1;
        }
        
        // Width/height values that take scale into account.
        // USE THESE FOR DRAWING POSITIONS
        let stageW, stageH;
        
        // All quality globals will be overwritten and updated via `configDidUpdate`.
        let quality = 1;
        let isLowQuality = false;
        let isNormalQuality = true;
        let isHighQuality = false;
        
        const QUALITY_LOW = 1;
        const QUALITY_NORMAL = 2;
        const QUALITY_HIGH = 3;
        
        const SKY_LIGHT_NONE = 0;
        const SKY_LIGHT_DIM = 1;
        const SKY_LIGHT_NORMAL = 2;
        
        const COLOR = {
        	Red: '#ff0043',
        	Green: '#14fc56',
        	Blue: '#1e7fff',
        	Purple: '#e60aff',
        	Gold: '#ffbf36',
        	White: '#ffffff'
        };
        
        // Special invisible color (not rendered, and therefore not in COLOR map)
        const INVISIBLE = '_INVISIBLE_';
        
        const PI_2 = Math.PI * 2;
        const PI_HALF = Math.PI * 0.5;
        
        // Stage.disableHighDPI = true;
        const trailsStage = new Stage('trails-canvas');
        const mainStage = new Stage('main-canvas');
        const stages = [
        	trailsStage,
        	mainStage
        ];
        
        
        
        // Fullscreen helpers, using Fscreen for prefixes.
        function fullscreenEnabled() {
        	return fscreen.fullscreenEnabled;
        }
        
        // Note that fullscreen state is synced to store, and the store should be the source
        // of truth for whether the app is in fullscreen mode or not.
        function isFullscreen() {
        	return !!fscreen.fullscreenElement;
        }
        
        // Attempt to toggle fullscreen mode.
        function toggleFullscreen() {
        	if (fullscreenEnabled()) {
        		if (isFullscreen()) {
        			fscreen.exitFullscreen();
        		} else {
        			fscreen.requestFullscreen(document.documentElement);
        		}
        	}
        }
        
        // Sync fullscreen changes with store. An event listener is necessary because the user can
        // toggle fullscreen mode directly through the browser, and we want to react to that.
        fscreen.addEventListener('fullscreenchange', () => {
        	store.setState({ fullscreen: isFullscreen() });
        });
        
        
        
        
        // Simple state container; the source of truth.
        const store = {
        	_listeners: new Set(),
        	_dispatch(prevState) {
        		this._listeners.forEach(listener => listener(this.state, prevState))
        	},
        	
        	state: {
        		// will be unpaused in init()
        		paused: true,
        		soundEnabled: false,
        		menuOpen: false,
        		openHelpTopic: null,
        		fullscreen: isFullscreen(),
        		// Note that config values used for <select>s must be strings, unless manually converting values to strings
        		// at render time, and parsing on change.
        		config: {
        			quality: String(IS_HIGH_END_DEVICE ? QUALITY_HIGH : QUALITY_NORMAL), // will be mirrored to a global variable named `quality` in `configDidUpdate`, for perf.
        			shell: 'Random',
        			size: IS_DESKTOP
        				? '3' // Desktop default
        				: IS_HEADER 
        					? '1.2' // Profile header default (doesn't need to be an int)
        					: '2', // Mobile default
        			autoLaunch: true,
        			finale: false,
        			skyLighting: SKY_LIGHT_NORMAL + '',
        			hideControls: IS_HEADER,
        			longExposure: false,
        			scaleFactor: getDefaultScaleFactor()
        		}
        	},
        	
        	setState(nextState) {
        		const prevState = this.state;
        		this.state = Object.assign({}, this.state, nextState);
        		this._dispatch(prevState);
        		this.persist();
        	},
        	
        	subscribe(listener) {
        		this._listeners.add(listener);
        		return () => this._listeners.remove(listener);
        	},
        	
        	// Load / persist select state to localStorage
        	// Mutates state because `store.load()` should only be called once immediately after store is created, before any subscriptions.
        	load() {
        		const serializedData = localStorage.getItem('cm_fireworks_data');
        		if (serializedData) {
        			const {
        				schemaVersion,
        				data
        			} = JSON.parse(serializedData);
        			
        			const config = this.state.config;
        			switch(schemaVersion) {
        				case '1.1':
        					config.quality = data.quality;
        					config.size = data.size;
        					config.skyLighting = data.skyLighting;
        					break;
        				case '1.2':
        					config.quality = data.quality;
        					config.size = data.size;
        					config.skyLighting = data.skyLighting;
        					config.scaleFactor = data.scaleFactor;
        					break;
        				default:
        					throw new Error('version switch should be exhaustive');
        			}
        			console.log(`Loaded config (schema version ${schemaVersion})`);
        		}
        		// Deprecated data format. Checked with care (it's not namespaced).
        		else if (localStorage.getItem('schemaVersion') === '1') {
        			let size;
        			// Attempt to parse data, ignoring if there is an error.
        			try {
        				const sizeRaw = localStorage.getItem('configSize');
        				size = typeof sizeRaw === 'string' && JSON.parse(sizeRaw);
        			}
        			catch(e) {
        				console.log('Recovered from error parsing saved config:');
        				console.error(e);
        				return;
        			}
        			// Only restore validated values
        			const sizeInt = parseInt(size, 10);
        			if (sizeInt >= 0 && sizeInt <= 4) {
        				this.state.config.size = String(sizeInt);
        			}
        		}
        	},
        	
        	persist() {
        		const config = this.state.config;
        		localStorage.setItem('cm_fireworks_data', JSON.stringify({
        			schemaVersion: '1.2',
        			data: {
        				quality: config.quality,
        				size: config.size,
        				skyLighting: config.skyLighting,
        				scaleFactor: config.scaleFactor
        			}
        		}));
        	}
        };
        
        
        if (!IS_HEADER) {
        	store.load();
        }
        
        // Actions
        // ---------
        
        function togglePause(toggle) {
        	const paused = store.state.paused;
        	let newValue;
        	if (typeof toggle === 'boolean') {
        		newValue = toggle;
        	} else {
        		newValue = !paused;
        	}
        
        	if (paused !== newValue) {
        		store.setState({ paused: newValue });
        	}
        }
        
        function toggleSound(toggle) {
        	if (typeof toggle === 'boolean') {
        		store.setState({ soundEnabled: toggle });
        	} else {
        		store.setState({ soundEnabled: !store.state.soundEnabled });
        	}
        }
        
        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)
        	});
        	
        	configDidUpdate();
        }
        
        // Map config to various properties & apply side effects
        function configDidUpdate() {
        	const config = store.state.config;
        	
        	quality = qualitySelector();
        	isLowQuality = quality === QUALITY_LOW;
        	isNormalQuality = quality === QUALITY_NORMAL;
        	isHighQuality = quality === QUALITY_HIGH;
        	
        	if (skyLightingSelector() === SKY_LIGHT_NONE) {
        		appNodes.canvasContainer.style.backgroundColor = '#000';
        	}
        	
        	Spark.drawWidth = quality === QUALITY_HIGH ? 0.75 : 1;
        }
        
        // Selectors
        // -----------
        
        const isRunning = (state=store.state) => !state.paused && !state.menuOpen;
        // Whether user has enabled sound.
        const soundEnabledSelector = (state=store.state) => state.soundEnabled;
        // Whether any sounds are allowed, taking into account multiple factors.
        const canPlaySoundSelector = (state=store.state) => isRunning(state) && soundEnabledSelector(state);
        // Convert quality to number.
        const qualitySelector = () => +store.state.config.quality;
        const shellNameSelector = () => store.state.config.shell;
        // Convert shell size to number.
        const shellSizeSelector = () => +store.state.config.size;
        const finaleSelector = () => store.state.config.finale;
        const skyLightingSelector = () => +store.state.config.skyLighting;
        const scaleFactorSelector = () => store.state.config.scaleFactor;
        
        
        
        // Help Content
        const helpContent = {
        	shellType: {
        		header: 'Shell Type',
        		body: 'The type of firework that will be launched. Select "Random" for a nice assortment!'
        	},
        	shellSize: {
        		header: 'Shell Size',
        		body: 'The size of the fireworks. Modeled after real firework shell sizes, larger shells have bigger bursts with more stars, and sometimes more complex effects. However, larger shells also require more processing power and may cause lag.'
        	},
        	quality: {
        		header: 'Quality',
        		body: 'Overall graphics quality. If the animation is not running smoothly, try lowering the quality. High quality greatly increases the amount of sparks rendered and may cause lag.'
        	},
        	skyLighting: {
        		header: 'Sky Lighting',
        		body: 'Illuminates the background as fireworks explode. If the background looks too bright on your screen, try setting it to "Dim" or "None".'
        	},
        	scaleFactor: {
        		header: 'Scale',
        		body: 'Allows scaling the size of all fireworks, essentially moving you closer or farther away. For larger shell sizes, it can be convenient to decrease the scale a bit, especially on phones or tablets.'
        	},
        	autoLaunch: {
        		header: 'Auto Fire',
        		body: 'Launches sequences of fireworks automatically. Sit back and enjoy the show, or disable to have full control.'
        	},
        	finaleMode: {
        		header: 'Finale Mode',
        		body: 'Launches intense bursts of fireworks. May cause lag. Requires "Auto Fire" to be enabled.'
        	},
        	hideControls: {
        		header: 'Hide Controls',
        		body: 'Hides the translucent controls along the top of the screen. Useful for screenshots, or just a more seamless experience. While hidden, you can still tap the top-right corner to re-open this menu.'
        	},
        	fullscreen: {
        		header: 'Fullscreen',
        		body: 'Toggles fullscreen mode.'
        	},
        	longExposure: {
        		header: 'Open Shutter',
        		body: 'Experimental effect that preserves long streaks of light, similar to leaving a camera shutter open.'
        	}
        };
        
        const nodeKeyToHelpKey = {
        	shellTypeLabel: 'shellType',
        	shellSizeLabel: 'shellSize',
        	qualityLabel: 'quality',
        	skyLightingLabel: 'skyLighting',
        	scaleFactorLabel: 'scaleFactor',
        	autoLaunchLabel: 'autoLaunch',
        	finaleModeLabel: 'finaleMode',
        	hideControlsLabel: 'hideControls',
        	fullscreenLabel: 'fullscreen',
        	longExposureLabel: 'longExposure'
        };
        
        
        // Render app UI / keep in sync with state
        const appNodes = {
        	stageContainer: '.stage-container',
        	canvasContainer: '.canvas-container',
        	controls: '.controls',
        	menu: '.menu',
        	menuInnerWrap: '.menu__inner-wrap',
        	pauseBtn: '.pause-btn',
        	pauseBtnSVG: '.pause-btn use',
        	soundBtn: '.sound-btn',
        	soundBtnSVG: '.sound-btn use',
        	shellType: '.shell-type',
        	shellTypeLabel: '.shell-type-label',
        	shellSize: '.shell-size',
        	shellSizeLabel: '.shell-size-label',
        	quality: '.quality-ui',
        	qualityLabel: '.quality-ui-label',
        	skyLighting: '.sky-lighting',
        	skyLightingLabel: '.sky-lighting-label',
        	scaleFactor: '.scaleFactor',
        	scaleFactorLabel: '.scaleFactor-label',
        	autoLaunch: '.auto-launch',
        	autoLaunchLabel: '.auto-launch-label',
        	finaleModeFormOption: '.form-option--finale-mode',
        	finaleMode: '.finale-mode',
        	finaleModeLabel: '.finale-mode-label',
        	hideControls: '.hide-controls',
        	hideControlsLabel: '.hide-controls-label',
        	fullscreenFormOption: '.form-option--fullscreen',
        	fullscreen: '.fullscreen',
        	fullscreenLabel: '.fullscreen-label',
        	longExposure: '.long-exposure',
        	longExposureLabel: '.long-exposure-label',
        	
        	// Help UI
        	helpModal: '.help-modal',
        	helpModalOverlay: '.help-modal__overlay',
        	helpModalHeader: '.help-modal__header',
        	helpModalBody: '.help-modal__body',
        	helpModalCloseBtn: '.help-modal__close-btn'
        };
        
        // Convert appNodes selectors to dom nodes
        Object.keys(appNodes).forEach(key => {
        	appNodes[key] = document.querySelector(appNodes[key]);
        });
        
        // Remove fullscreen control if not supported.
        if (!fullscreenEnabled()) {
        	appNodes.fullscreenFormOption.classList.add('remove');
        }
        
        // First render is called in init()
        function renderApp(state) {
        	const pauseBtnIcon = `#icon-${state.paused ? 'play' : 'pause'}`;
        	const soundBtnIcon = `#icon-sound-${soundEnabledSelector() ? 'on' : 'off'}`;
        	appNodes.pauseBtnSVG.setAttribute('href', pauseBtnIcon);
        	appNodes.pauseBtnSVG.setAttribute('xlink:href', pauseBtnIcon);
        	appNodes.soundBtnSVG.setAttribute('href', soundBtnIcon);
        	appNodes.soundBtnSVG.setAttribute('xlink:href', soundBtnIcon);
        	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.finaleModeFormOption.style.opacity = state.config.autoLaunch ? 1 : 0.32;
        	
        	appNodes.quality.value = state.config.quality;
        	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.skyLighting.value = state.config.skyLighting;
        	appNodes.hideControls.checked = state.config.hideControls;
        	appNodes.fullscreen.checked = state.fullscreen;
        	appNodes.longExposure.checked = state.config.longExposure;
        	appNodes.scaleFactor.value = state.config.scaleFactor.toFixed(2);
        	
        	appNodes.menuInnerWrap.style.opacity = state.openHelpTopic ? 0.12 : 1;
        	appNodes.helpModal.classList.toggle('active', !!state.openHelpTopic);
        	if (state.openHelpTopic) {
        		const { header, body } = helpContent[state.openHelpTopic];
        		appNodes.helpModalHeader.textContent = header;
        		appNodes.helpModalBody.textContent = body;
        	}
        }
        
        store.subscribe(renderApp);
        
        // Perform side effects on state changes
        function handleStateChange(state, prevState) {
        	const canPlaySound = canPlaySoundSelector(state);
        	const canPlaySoundPrev = canPlaySoundSelector(prevState);
        	
        	if (canPlaySound !== canPlaySoundPrev) {
        		if (canPlaySound) {
        			soundManager.resumeAll();
        		} else {
        			soundManager.pauseAll();
        		}
        	}
        }
        
        store.subscribe(handleStateChange);
        
        
        function getConfigFromDOM() {
        	return {
        		quality: appNodes.quality.value,
        		shell: appNodes.shellType.value,
        		size: appNodes.shellSize.value,
        		autoLaunch: appNodes.autoLaunch.checked,
        		finale: appNodes.finaleMode.checked,
        		skyLighting: appNodes.skyLighting.value,
        		longExposure: appNodes.longExposure.checked,
        		hideControls: appNodes.hideControls.checked,
        		// Store value as number.
        		scaleFactor: parseFloat(appNodes.scaleFactor.value)
        	};
        };
        
        const updateConfigNoEvent = () => updateConfig();
        appNodes.quality.addEventListener('input', updateConfigNoEvent);
        appNodes.shellType.addEventListener('input', updateConfigNoEvent);
        appNodes.shellSize.addEventListener('input', updateConfigNoEvent);
        appNodes.autoLaunch.addEventListener('click', () => setTimeout(updateConfig, 0));
        appNodes.finaleMode.addEventListener('click', () => setTimeout(updateConfig, 0));
        appNodes.skyLighting.addEventListener('input', updateConfigNoEvent);
        appNodes.longExposure.addEventListener('click', () => setTimeout(updateConfig, 0));
        appNodes.hideControls.addEventListener('click', () => setTimeout(updateConfig, 0));
        appNodes.fullscreen.addEventListener('click', () => setTimeout(toggleFullscreen, 0));
        // Changing scaleFactor requires triggering resize handling code as well.
        appNodes.scaleFactor.addEventListener('input', () => {
        	updateConfig();
        	handleResize();
        });
        
        Object.keys(nodeKeyToHelpKey).forEach(nodeKey => {
        	const helpKey = nodeKeyToHelpKey[nodeKey];
        	appNodes[nodeKey].addEventListener('click', () => {
        		store.setState({ openHelpTopic: helpKey });
        	});
        });
        
        appNodes.helpModalCloseBtn.addEventListener('click', () => {
        	store.setState({ openHelpTopic: null });
        });
        
        appNodes.helpModalOverlay.addEventListener('click', () => {
        	store.setState({ openHelpTopic: null });
        });
        
        
        
        // 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];
        // Map of color codes to their index in the array. Useful for quickly determining if a color has already been updated in a loop.
        const COLOR_CODE_INDEXES = COLOR_CODES_W_INVIS.reduce((obj, code, i) => {
        	obj[code] = i;
        	return obj;
        }, {});
        // 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;
        }
        
        
        // 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.72;
        	const color = singleColor ? randomColor({ limitWhite: true }) : [randomColor(), randomColor({ notSame: true })];
        	const pistil = singleColor && Math.random() < 0.42;
        	const pistilColor = pistil && makePistilColor(color);
        	const secondColor = singleColor && (Math.random() < 0.2 || color === COLOR.White) ? pistilColor || randomColor({ notColor: color, limitWhite: true }) : null;
        	const streamers = !pistil && color !== COLOR.White && Math.random() < 0.42;
        	let starDensity = glitter ? 1.1 : 1.25;
        	if (isLowQuality) starDensity *= 0.8;
        	if (isHighQuality) starDensity = 1.2;
        	return {
        		shellSize: size,
        		spreadSize: 300 + size * 100,
        		starLife: 900 + size * 200,
        		starDensity,
        		color,
        		secondColor,
        		glitter: glitter ? 'light' : '',
        		glitterColor: whiteOrGold(),
        		pistil,
        		pistilColor,
        		streamers
        	};
        };
        
        
        const ghostShell = (size=1) => {
        	// Extend crysanthemum shell
        	const shell = crysanthemumShell(size);
        	// Ghost effect can be fast, so extend star life
        	shell.starLife *= 1.5;
        	// Ensure we always have a single color other than white
        	let ghostColor = randomColor({ notColor: COLOR.White });
        	// Always use streamers, and sometimes a pistil
        	shell.streamers = true;
        	const pistil = Math.random() < 0.42;
        	const pistilColor = pistil && makePistilColor(ghostColor);
        	// Ghost effect - transition from invisible to chosen color
        	shell.color = INVISIBLE;
        	shell.secondColor = ghostColor;
        	// We don't want glitter to be spewed by invisible stars, and we don't currently
        	// have a way to transition glitter state. So we'll disable it.
        	shell.glitter = '';
        	
        	return shell;
        };
        
        
        const strobeShell = (size=1) => {
        	const color = randomColor({ limitWhite: true });
        	return {
        		shellSize: size,
        		spreadSize: 280 + size * 92,
        		starLife: 1100 + size * 200,
        		starLifeVariation: 0.40,
        		starDensity: 1.1,
        		color,
        		glitter: 'light',
        		glitterColor: COLOR.White,
        		strobe: true,
        		strobeColor: Math.random() < 0.5 ? COLOR.White : null,
        		pistil: Math.random() < 0.5,
        		pistilColor: makePistilColor(color)
        	};
        };
        
        
        const palmShell = (size=1) => {
        	const color = randomColor();
        	const thick = Math.random() < 0.5;
        	return {
        		shellSize: size,
        		color,
        		spreadSize: 250 + size * 75,
        		starDensity: thick ? 0.15 : 0.4,
        		starLife: 1800 + size * 200,
        		glitter: thick ? 'thick' : 'heavy'
        	};
        };
        
        const ringShell = (size=1) => {
        	const color = randomColor();
        	const pistil = Math.random() < 0.75;
        	return {
        		shellSize: size,
        		ring: true,
        		color,
        		spreadSize: 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,
        		streamers: Math.random() < 0.3
        	};
        	// return Object.assign({}, defaultShell, config);
        };
        
        const crossetteShell = (size=1) => {
        	const color = randomColor({ limitWhite: true });
        	return {
        		shellSize: size,
        		spreadSize: 300 + size * 100,
        		starLife: 750 + size * 160,
        		starLifeVariation: 0.4,
        		starDensity: 0.85,
        		color,
        		crossette: true,
        		pistil: Math.random() < 0.5,
        		pistilColor: makePistilColor(color)
        	};
        };
        
        const floralShell = (size=1) => ({
        	shellSize: size,
        	spreadSize: 300 + size * 120,
        	starDensity: 0.12,
        	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) => ({
        	shellSize: size,
        	color: INVISIBLE,
        	spreadSize: 300 + size * 120,
        	starDensity: 0.12,
        	starLife: 500 + size * 50,
        	starLifeVariation: 0.5,
        	glitter: 'medium',
        	glitterColor: COLOR.Gold,
        	fallingLeaves: true
        });
        
        const willowShell = (size=1) => ({
        	shellSize: size,
        	spreadSize: 300 + size * 100,
        	starDensity: 0.6,
        	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 {
        		shellSize: size,
        		spreadSize: 380 + size * 75,
        		starDensity: isLowQuality ? 0.65 : 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 {
        		shellSize: size,
        		horsetail: true,
        		color,
        		spreadSize: 250 + size * 38,
        		starDensity: 0.9,
        		starLife: 2500 + size * 300,
        		glitter: 'medium',
        		glitterColor: Math.random() < 0.5 ? whiteOrGold() : color,
        		// Add strobe effect to white horsetails, to make them more interesting
        		strobe: color === COLOR.White
        	};
        };
        
        function randomShellName() {
        	return Math.random() < 0.5 ? 'Crysanthemum' : shellNames[(Math.random() * (shellNames.length - 1) + 1) | 0 ];
        }
        
        function randomShell(size) {
        	// Special selection for codepen header.
        	if (IS_HEADER) return randomFastShell()(size);
        	// Normal operation
        	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,
        	'Ghost': ghostShell,
        	'Horse Tail': horsetailShell,
        	'Palm': palmShell,
        	'Ring': ringShell,
        	'Strobe': strobeShell,
        	'Willow': willowShell
        };
        
        const shellNames = Object.keys(shellTypes);
        
        function init() {
        	// Remove loading state
        	document.querySelector('.loading-init').remove();
        	appNodes.stageContainer.classList.remove('remove');
        	
        	// Populate dropdowns
        	function setOptionsForSelect(node, options) {
        		node.innerHTML = options.reduce((acc, opt) => acc += `<option value="${opt.value}">${opt.label}</option>`, '');
        	}
        
        	// shell type
        	let options = '';
        	shellNames.forEach(opt => options += `<option value="${opt}">${opt}</option>`);
        	appNodes.shellType.innerHTML = options;
        	// shell size
        	options = '';
        	['3"', '4"', '6"', '8"', '12"', '16"'].forEach((opt, i) => options += `<option value="${i}">${opt}</option>`);
        	appNodes.shellSize.innerHTML = options;
        	
        	setOptionsForSelect(appNodes.quality, [
        		{ label: 'Low', value: QUALITY_LOW },
        		{ label: 'Normal', value: QUALITY_NORMAL },
        		{ label: 'High', value: QUALITY_HIGH }
        	]);
        	
        	setOptionsForSelect(appNodes.skyLighting, [
        		{ label: 'None', value: SKY_LIGHT_NONE },
        		{ label: 'Dim', value: SKY_LIGHT_DIM },
        		{ label: 'Normal', value: SKY_LIGHT_NORMAL }
        	]);
        	
        	// 0.9 is mobile default
        	setOptionsForSelect(
        		appNodes.scaleFactor,
        		[0.5, 0.62, 0.75, 0.9, 1.0, 1.5, 2.0]
        		.map(value => ({ value: value.toFixed(2), label: `${value*100}%` }))
        	);
        	
        	// Begin simulation
        	togglePause(false);
        	
        	// initial render
        	renderApp(store.state);
        	
        	// Apply initial config
        	configDidUpdate();
        }
        
        
        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 = 4600;
        	}
        	
        	return 900 + Math.random() * 600 + extraDelay;
        }
        
        function seqRandomFastShell() {
        	const shellType = randomFastShell();
        	const size = getRandomShellSize();
        	const shell = new Shell(shellType(size.size));
        	shell.launch(size.x, size.height);
        	
        	let extraDelay = shell.starLife;
        	
        	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));
      .........完整代码请登录后点击上方下载按钮下载查看

网友评论0