UNPKG

littlejsengine

Version:

LittleJS - Tiny and Fast HTML5 Game Engine

399 lines (356 loc) 14.4 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; } </style> </head><body> <script src=../../dist/littlejs.js?1117></script> <script> 'use strict'; // fix texture bleeding by shrinking tiles slightly tileFixBleedScale = .5; let particleEditor = 0, particleSystem, particleSystemDiv, particleSystemCode; let inputExpand; let restartTimer = new 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('collideTiles', 'checkbox',0,0,0, 'Do particles collide against tiles?'); 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 ParticleEmitter(cameraPos); particleSystem.tileInfo = tile(); particleSystem.emitConeAngle = particleSystem.particleConeAngle = 3.14; } /////////////////////////////////////////////////////////////////////////////// function gameInit() { makeEmitter(); gravity = -.01; cameraScale = 64; const div = particleSystemDiv = document.createElement('div'); div.innerHTML = '<big><b>LittleJS Particle Tool'; div.style = 'position:absolute;top:10px;left:10px;color:#fff'; document.body.appendChild(div); 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(0); } 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(); cameraScale = 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 = '500px'; 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 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 = clamp(inputFloat); else if (name == 'colorStartB_alpha') particleSystem.colorStartB.a = clamp(inputFloat); else if (name == 'colorEndA_alpha') particleSystem.colorEndA.a = clamp(inputFloat); else if (name == 'colorEndB_alpha') particleSystem.colorEndB.a = clamp(inputFloat); } else if (name == 'tileIndex') { const tileIndex = parseInt(input.value); particleSystem.tileIndex = tileIndex particleSystem.tileInfo = 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] = min(inputFloat,1e3); else particleSystem[name] = 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(0); } 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 = ''; // https://www.codingem.com/javascript-how-to-limit-decimal-places/ Number.prototype.round = function(n) { const d = Math.pow(10, n); return Math.round((this + Number.EPSILON) * d) / d; } 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]; const p = 3; value = `new Color(${c.r.round(p)}, ${c.g.round(p)}, ${c.b.round(p)}, ${c.a.round(p)})`; } else if (type == 'checkbox') value = particleSystem[name] ? 1 : 0; else value = particleSystem[name].round(3); 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) { particleSystem.pos = mouseIsDown(0) ? mousePos : vec2(); if (mouseWheel) { cameraScale -= sign(mouseWheel)*cameraScale/5; cameraScale = clamp(cameraScale, 10, 300); } } } /////////////////////////////////////////////////////////////////////////////// function gameUpdatePost() { } /////////////////////////////////////////////////////////////////////////////// function gameRender() { } /////////////////////////////////////////////////////////////////////////////// function gameRenderPost() { } /////////////////////////////////////////////////////////////////////////////// // Startup LittleJS Engine engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']); </script>