UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

426 lines (382 loc) 15 kB
<!DOCTYPE html><head> <title>LittleJS Particle System Designer</title> <meta charset=utf-8> <meta name=apple-mobile-web-app-capable content=yes> <meta name=mobile-web-app-capable content=yes> <link rel=icon type=image/png href=../favicon.png> <style> input { width:99px; } button { margin:1px; } body { color:#fff; } #div_emitterSettings { position:absolute; z-index: 1; height: 100vh; overflow:auto; padding:5px; font-family:sans-serif; column-count: 2; column-fill: auto; } </style> </head><body> <div id=div_emitterSettings></div> <script type=module> // import LittleJS module import * as LJS from '../../dist/littlejs.esm.js'; const {vec2} = LJS; 'use strict'; // fix texture bleeding by shrinking tiles slightly LJS.setTileFixBleedScale(.5); // allow html input controls LJS.setInputPreventDefault(false); let particleEditor = 0, particleSystem, particleSystemDiv, particleSystemCode; let inputExpand; let restartTimer = new LJS.Timer(); const storagePrefix = 'particles_'; const particleSettings = []; function addSetting(name, type, min, max, step, description) { particleSettings.push({name, type:type||'number', min, max, step, description}); } addSetting('emitSize', '', 0, 1e9, .1, 'World space size of the emitter'); addSetting('emitTime', '', 0, 1e9, .1, 'How long to stay alive (0 is forever)'); addSetting('emitRate', '', 0, 1e9, 1, 'How many particles per second to spawn, does not emit if 0'); addSetting('emitConeAngle', '', 0, 1e9, .01,'Local angle to apply velocity to particles from emitter'); addSetting('tileIndex', '', -1, 1e9, 1, 'Tile to use to render object (-1 is untextured)'); addSetting('tileSize', '', 0, 1e9, 1, 'Size of texture tile in source pixels'); addSetting('colorStartA', 'color',0,0,0, 'Color at start of life 1'); addSetting('colorStartA_alpha', 'alpha', 0, 1, .01, 'Alpha at start of life 1'); addSetting('colorStartB', 'color',0,0,0, 'Color at start of life 2'); addSetting('colorStartB_alpha', 'alpha', 0, 1, .01, 'Alpha at start of life 2'); addSetting('colorEndA', 'color',0,0,0, 'Color at end of life 1'); addSetting('colorEndA_alpha', 'alpha', 0, 1, .01, 'Alpha at end of life 1'); addSetting('colorEndB', 'color',0,0,0, 'Color at end of life 2'); addSetting('colorEndB_alpha', 'alpha', 0, 1, .01, 'Alpha at end of life 2'); addSetting('particleTime', '', 0, 1e9, .1, 'How long particles live'); addSetting('sizeStart', '', 0, 1e9, .01,'How big are particles at start'); addSetting('sizeEnd', '', 0, 1e9, .01,'How big are particles at end'); addSetting('speed', '', 0, 1e9, .01,'How fast are particles when spawned'); addSetting('angleSpeed', '', 0, 1e9, .01,'How fast are particles rotating'); addSetting('damping', '', 0, 1, .01,'How much to dampen particle speed'); addSetting('angleDamping', '', 0, 1, .01,'How much to dampen particle angular speed'); addSetting('gravityScale', '', -1e9, 1e9, .1, 'How much does gravity effect particles'); addSetting('particleConeAngle','', 0, 1e9, .1, 'Cone for start particle angle'); addSetting('fadeRate', '', 0, 1, .01,'How quickly particles fade in percent of life'); addSetting('randomness', '', 0, 1, .01,'Apply extra randomness percent'); addSetting('additive', 'checkbox',0,0,0, 'Should particles use addtive blend?'); addSetting('randomColorLinear','checkbox',0,0,0, 'Should color be randomized linearly?'); function makeEmitter() { if (particleSystem) particleSystem.destroy(); particleSystem = new LJS.ParticleEmitter(LJS.cameraPos); particleSystem.tileInfo = LJS.tile(); particleSystem.emitConeAngle = particleSystem.particleConeAngle = 3.14; } /////////////////////////////////////////////////////////////////////////////// function gameInit() { makeEmitter(); LJS.setGravity(vec2(0,-.01)); LJS.setCameraScale(64); const div = div_emitterSettings; const title = document.createElement('span') title.innerText = 'LittleJS Particle Tool'; title.style = 'font-size:20px;font-weight:bold;'; div.appendChild(title); for (const setting of particleSettings) { // get default value // todo: clean this up let defaultValue = ''; { const name = setting.name; const type = setting.type; if (type == 'color') { const color = particleSystem[name]; defaultValue = color.toString(false); } else if (type == 'alpha') { if (name == 'colorStartA_alpha') defaultValue = particleSystem.colorStartA.a; else if (name == 'colorStartB_alpha') defaultValue = particleSystem.colorStartB.a; else if (name == 'colorEndA_alpha') defaultValue = particleSystem.colorEndA.a; else if (name == 'colorEndB_alpha') defaultValue = particleSystem.colorEndB.a; } else if (name == 'tileSize') defaultValue = particleSystem.tileInfo.size.x; else if (type == 'checkbox') defaultValue = particleSystem[name]; else defaultValue = particleSystem[name] || '0'; } div.appendChild(document.createElement('br')); const input = setting.input = document.createElement('input'); const name = setting.name; input.type = setting.type == 'alpha' ? 'number' : setting.type; input.title = setting.description; input.oninput = (e)=> updateParticles(setting.type == 'checkbox'); input.onblur = (e)=> updateParticles(1); if (setting.min !== undefined) input.min = setting.min; if (setting.max !== undefined) input.max = setting.max; if (setting.step !== undefined) input.step = setting.step; if (name == 'emitConeAngle' || name == 'particleConeAngle') particleSystem[name] = 3.14; // simplify pi const inputReset = document.createElement('button'); inputReset.innerText = '♻️'; inputReset.onclick = (e)=> { setting.type == 'checkbox'? setting.input.checked = defaultValue : setting.input.value = defaultValue; updateParticles(1); } div.appendChild(inputReset); div.appendChild(input); div.appendChild(document.createTextNode(' ' + name)); } div.appendChild(document.createElement('br')); div.appendChild(document.createElement('br')); { const button = document.createElement('button') div.appendChild(button); button.innerHTML = 'Copy To Clipboard'; button.onclick = (e)=> navigator.clipboard.writeText(particleSystemCode.value); } { const button = document.createElement('button'); div.appendChild(button); button.innerHTML = 'Reset'; button.onclick = (e)=> { if (!confirm('Reset to default?')) return; makeEmitter(); updateInputs(); LJS.setCameraScale(50); } } { inputExpand = document.createElement('input'); div.appendChild(inputExpand); inputExpand.type = 'checkbox'; inputExpand.style.width = 'initial'; inputExpand.oninput = (e)=> updateParticles(1); div.appendChild(document.createTextNode(' Expand')); } //div.appendChild(document.createTextNode('JavaScript Code... ')); div.appendChild(document.createElement('br')); div.appendChild(particleSystemCode = document.createElement('textarea')); particleSystemCode.style.width = '300px'; particleSystemCode.style.height = '100px'; particleSystemCode.disabled = true; updateInputs(0); // load from storage for (const setting of particleSettings) { const type = setting.type; const storageName = storagePrefix + setting.name; const savedValue = localStorage[storageName]; if (savedValue == undefined) continue; if (type == 'checkbox') setting.input.checked = savedValue == 'true'; else setting.input.value = savedValue; } updateParticles(); particleSystemCode.value = getCode(); } function updateParticles(shouldUpdateInput) { for (const setting of particleSettings) { const input = setting.input; const name = setting.name; const type = setting.type; const inputFloat = parseFloat(input.value) || 0; if (type == 'color') { const color = new LJS.Color().setHex(input.value); particleSystem[name].r = color.r; particleSystem[name].g = color.g; particleSystem[name].b = color.b; } else if (type == 'alpha') { if (name == 'colorStartA_alpha') particleSystem.colorStartA.a = LJS.clamp(inputFloat); else if (name == 'colorStartB_alpha') particleSystem.colorStartB.a = LJS.clamp(inputFloat); else if (name == 'colorEndA_alpha') particleSystem.colorEndA.a = LJS.clamp(inputFloat); else if (name == 'colorEndB_alpha') particleSystem.colorEndB.a = LJS.clamp(inputFloat); } else if (name == 'tileIndex') { const tileIndex = parseInt(input.value); particleSystem.tileIndex = tileIndex particleSystem.tileInfo = LJS.tile(tileIndex, particleSystem.tileInfo.size); } else if (name == 'tileSize') particleSystem.tileInfo.size = vec2(parseInt(input.value)); else if (type == 'checkbox') particleSystem[name] = input.checked ? 1 : 0; else if (name == 'emitRate') particleSystem[name] = LJS.min(inputFloat,1e4); else particleSystem[name] = LJS.clamp(inputFloat, setting.min, setting.max); } shouldUpdateInput && updateInputs() } function updateInputs(save=1) { for (const setting of particleSettings) updateSetting(setting, save); particleSystemCode.value = getCode(); } function updateSetting(setting, save=1) { const input = setting.input; const name = setting.name; const type = setting.type; if (type == 'color') { const color = particleSystem[name]; input.value = color.toString(false); } else if (type == 'alpha') { if (name == 'colorStartA_alpha') input.value = particleSystem.colorStartA.a; else if (name == 'colorStartB_alpha') input.value = particleSystem.colorStartB.a; else if (name == 'colorEndA_alpha') input.value = particleSystem.colorEndA.a; else if (name == 'colorEndB_alpha') input.value = particleSystem.colorEndB.a; } else if (name == 'tileSize') input.value = particleSystem.tileInfo.size.x; else if (type == 'checkbox') input.checked = particleSystem[name]; else input.value = particleSystem[name] || '0'; if (save) { const storageName = storagePrefix + setting.name; localStorage[storageName] = type == 'checkbox' ? input.checked : input.value; } } function getCode() { const expand = inputExpand.checked; let code = ''; // limit float precision to prevent long strings const trimFloat = (n, decimals=3)=> Number(Number(n).toFixed(decimals)); code = 'new ParticleEmitter('; if (expand) code += '\n vec2(), 0,\t//position, angle\n'; else code += 'vec2(), 0, '; let count = 0; for (const setting of particleSettings) { const name = setting.name; const type = setting.type; let value; if (name == 'tileSize' || type == 'alpha') continue; if (name == 'tileIndex') value = `tile(${particleSystem.tileIndex}, ${particleSystem.tileInfo.size.x})`; else if (type == 'color') { const c = particleSystem[name]; value = `new Color(${trimFloat(c.r)}, ${trimFloat(c.g)}, ${trimFloat(c.b)}, ${trimFloat(c.a)})`; } else if (type == 'checkbox') value = particleSystem[name] ? 1 : 0; else value = trimFloat(particleSystem[name]); if (expand) code += ' '; code += value; const isEnd = name == 'randomColorLinear'; if (expand) { code += ',\t// ' + name; if (!isEnd) code += '\n'; } else if (!isEnd) code += ', '; } if (expand) code += '\n); // particle emitter'; else code += ');'; return code; } /////////////////////////////////////////////////////////////////////////////// function gameUpdate() { if (!restartTimer.isSet() && particleSystem.destroyed) restartTimer.set(1); else if (restartTimer.elapsed()) { restartTimer.unset() makeEmitter(); updateParticles(); } if (document.activeElement == document.body) { if (LJS.mouseIsDown(0)) { particleSystem.pos = LJS.mousePos; particleSystem.velocity = LJS.mouseDelta.scale(.3*LJS.timeDelta); } else { particleSystem.pos = vec2(); particleSystem.velocity = vec2(); } if (LJS.mouseWheel) { // zoom camera let cameraScale = LJS.cameraScale; cameraScale -= LJS.sign(LJS.mouseWheel)*cameraScale/5; cameraScale = LJS.clamp(cameraScale, 10, 300); LJS.setCameraScale(cameraScale); } } } /////////////////////////////////////////////////////////////////////////////// function gameUpdatePost() { } /////////////////////////////////////////////////////////////////////////////// function gameRender() { } /////////////////////////////////////////////////////////////////////////////// function gameRenderPost() { } /////////////////////////////////////////////////////////////////////////////// // Startup LittleJS Engine LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); </script>