littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
426 lines (382 loc) • 15 kB
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;
;
// 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>