littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
399 lines (356 loc) • 14.4 kB
HTML
<!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>