赞
踩
全屏烟花动画特效,跨年新春烟花漫天效果。无加密,完整可用哦
1.桌面新建文本文档.txt
2.复制粘贴下面的代码
3.保存文件并修改文件的后缀为.html
4.点开生成的html文件就可以放一场电子烟花
- <!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;//是否使用单一颜色
- //一个颜色数组,包含1到2个颜色值。如果singleColor为真,则该数组仅包含一个颜色;否则该数组将包含两个不同的颜色。颜色值通过调用randomColor函数随机生成
- 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
- });
-
- //生成爆裂声烟花(crackleShell)效果
- 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 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);
- }
- };
-
- function init() {
- // Populate dropdowns
- // shell type
- let options = '';
- shellNames.forEach(opt => options += options += '<option value="' + opt + '">' + opt + '</option>');
- appNodes.shellType.innerHTML = options;
- // shell size
- options = '';
- ['3"', '5"', '6"', '8"', '12"'].forEach((opt, i) => options += '<option value="' + opt + '">' + opt + '</option>');
- appNodes.shellSize.innerHTML = options;
-
- renderApp(store.state);
- }
-
- </script>
-
- </body>
- </html>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。