littlejsengine 1.21.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/littlejs.d.ts +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- package/src/engineWebGL.js +27 -10
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Particle Effects Plugin
|
|
3
|
+
* - Ready made particle effects in one line, particleEffect('fire', pos), and particleEffect3D for 3D
|
|
4
|
+
* - Every built-in effect is tuned around a one unit emitter; options.scale grows it to fit, options.hue and
|
|
5
|
+
* options.saturation recolor it
|
|
6
|
+
* - Effects are plain data, {name, settings, behaviors}, the format the particle designer saves: load a library the
|
|
7
|
+
* designer saved with particleEffectsLoad and play its effects by name
|
|
8
|
+
* - The built-in effects draw with a sheet of shapes the plugin draws itself, so they need no image
|
|
9
|
+
* @namespace ParticleEffects
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
// settings, what an effect sets on an emitter
|
|
16
|
+
|
|
17
|
+
/** The groups the settings are in, in order
|
|
18
|
+
* @type {Array<string>}
|
|
19
|
+
* @memberof ParticleEffects */
|
|
20
|
+
const particleEffectGroups = ['Emitter', 'Particles', 'Color', 'Motion', 'Collision', 'Texture', 'Behaviors'];
|
|
21
|
+
|
|
22
|
+
/** Every setting an effect has: its name, kind (number, checkbox, color or shape), default, the range the designer's
|
|
23
|
+
* slider covers (min, max, step), the hard limits a typed value is clamped to, and whether it is set after the
|
|
24
|
+
* emitter is made rather than passed to its constructor (extra)
|
|
25
|
+
* @type {Array<{group:string, name:string, kind:string, value:any, min:number, max:number, step:number,
|
|
26
|
+
* description:string, hardMin:number, hardMax:number, extra:boolean}>}
|
|
27
|
+
* @memberof ParticleEffects */
|
|
28
|
+
const particleEffectSettings = [];
|
|
29
|
+
let particleEffectSettingGroup; // the group settings are being added to
|
|
30
|
+
|
|
31
|
+
// the slider covers min to max, typed values may go out to hardMin and hardMax
|
|
32
|
+
function particleEffectAddSetting(name, kind, value, min, max, step, description, hardMin=min, hardMax=max)
|
|
33
|
+
{
|
|
34
|
+
// extras are not constructor arguments, the code export assigns them after
|
|
35
|
+
const extra = ['trailScale', 'velocityInheritance', 'localSpace', 'restitution', 'friction', 'gravity'].includes(name);
|
|
36
|
+
particleEffectSettings.push({group:particleEffectSettingGroup, name, kind, value, min, max, step, description,
|
|
37
|
+
hardMin, hardMax, extra});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
particleEffectSettingGroup = 'Emitter';
|
|
41
|
+
particleEffectAddSetting('emitRate', 'number', 100, 0, 500, .1,
|
|
42
|
+
'Particles per second, 0 emits none', 0, 1e4);
|
|
43
|
+
particleEffectAddSetting('emitTime', 'number', 0, 0, 5, .05,
|
|
44
|
+
'Seconds to emit for, 0 is forever', 0, 1e9);
|
|
45
|
+
particleEffectAddSetting('emitSize', 'number', 0, 0, 10, .05,
|
|
46
|
+
'Diameter, or width when rectangular', 0, 1e9);
|
|
47
|
+
particleEffectAddSetting('emitRect', 'checkbox', false, 0, 0, 0,
|
|
48
|
+
'Rectangle emitter, off is a circle');
|
|
49
|
+
particleEffectAddSetting('emitHeight', 'number', 0, 0, 10, .05,
|
|
50
|
+
'Rectangle height', 0, 1e9);
|
|
51
|
+
particleEffectAddSetting('emitConeAngle', 'number', PI, 0, PI, .01,
|
|
52
|
+
'Half angle particles move in, 3.14 is all ways');
|
|
53
|
+
particleEffectAddSetting('angle', 'number', 0, -3.14, 3.14, .01,
|
|
54
|
+
'Emitter angle, 0 points up', -PI, PI); // the slider steps from its min
|
|
55
|
+
|
|
56
|
+
particleEffectSettingGroup = 'Particles';
|
|
57
|
+
particleEffectAddSetting('particleTime', 'number', .5, 0, 5, .05,
|
|
58
|
+
'Seconds each particle lives', 0, 1e9);
|
|
59
|
+
particleEffectAddSetting('sizeStart', 'number', .1, 0, 5, .01,
|
|
60
|
+
'Size at the start of life', 0, 1e9);
|
|
61
|
+
particleEffectAddSetting('sizeEnd', 'number', 1, 0, 5, .01,
|
|
62
|
+
'Size at the end of life', 0, 1e9);
|
|
63
|
+
particleEffectAddSetting('fadeRate', 'number', .1, 0, 1, .01,
|
|
64
|
+
'Share of life spent fading in and out');
|
|
65
|
+
particleEffectAddSetting('randomness', 'number', .2, 0, 1, .01,
|
|
66
|
+
'How much each particle varies');
|
|
67
|
+
particleEffectAddSetting('particleConeAngle', 'number', PI, 0, PI, .01,
|
|
68
|
+
'Half angle of start rotation, 3.14 is any');
|
|
69
|
+
particleEffectAddSetting('trailScale', 'number', 0, 0, 20, .1,
|
|
70
|
+
'Stretch along the motion, 0 is off', 0, 1e9);
|
|
71
|
+
particleEffectAddSetting('additive', 'checkbox', false, 0, 0, 0,
|
|
72
|
+
'Glow with additive blending');
|
|
73
|
+
|
|
74
|
+
particleEffectSettingGroup = 'Color';
|
|
75
|
+
particleEffectAddSetting('colorStartA', 'color', [1,1,1,1], 0, 1, .01,
|
|
76
|
+
'Start color, each particle picks between A and B');
|
|
77
|
+
particleEffectAddSetting('colorStartB', 'color', [1,1,1,1], 0, 1, .01,
|
|
78
|
+
'Second start color');
|
|
79
|
+
particleEffectAddSetting('colorEndA', 'color', [1,1,1,0], 0, 1, .01,
|
|
80
|
+
'End color, each particle picks between A and B');
|
|
81
|
+
particleEffectAddSetting('colorEndB', 'color', [1,1,1,0], 0, 1, .01,
|
|
82
|
+
'Second end color');
|
|
83
|
+
particleEffectAddSetting('randomColorLinear', 'checkbox', true, 0, 0, 0,
|
|
84
|
+
'Pick along A to B, off picks each channel');
|
|
85
|
+
|
|
86
|
+
particleEffectSettingGroup = 'Motion';
|
|
87
|
+
particleEffectAddSetting('speed', 'number', .1, 0, .5, .005,
|
|
88
|
+
'Start speed, world units per frame', 0, 1e9);
|
|
89
|
+
particleEffectAddSetting('angleSpeed', 'number', .05, 0, .5, .005,
|
|
90
|
+
'Spin speed, radians per frame', 0, 1e9);
|
|
91
|
+
particleEffectAddSetting('damping', 'number', 1, .8, 1, .001,
|
|
92
|
+
'Speed kept each frame, 1 keeps it all', 0, 1);
|
|
93
|
+
particleEffectAddSetting('angleDamping', 'number', 1, .8, 1, .001,
|
|
94
|
+
'Spin kept each frame', 0, 1);
|
|
95
|
+
particleEffectAddSetting('gravityScale', 'number', 0, -2, 2, .05,
|
|
96
|
+
'How much gravity pulls', -1e9, 1e9);
|
|
97
|
+
particleEffectAddSetting('gravity', 'number', 0, -.02, .02, .0005,
|
|
98
|
+
'Its own fall each frame, the same in any game', -1, 1);
|
|
99
|
+
particleEffectAddSetting('velocityInheritance', 'number', 0, 0, 1, .01,
|
|
100
|
+
'Share of emitter motion passed on');
|
|
101
|
+
particleEffectAddSetting('localSpace', 'checkbox', false, 0, 0, 0,
|
|
102
|
+
'Particles move with the emitter');
|
|
103
|
+
|
|
104
|
+
particleEffectSettingGroup = 'Collision';
|
|
105
|
+
particleEffectAddSetting('collideLevel', 'checkbox', false, 0, 0, 0,
|
|
106
|
+
'Hit tiles, the preview adds a floor');
|
|
107
|
+
particleEffectAddSetting('restitution', 'number', 0, 0, 1, .01,
|
|
108
|
+
'Bounce when hitting tiles');
|
|
109
|
+
particleEffectAddSetting('friction', 'number', .8, 0, 1, .01,
|
|
110
|
+
'Speed kept sliding along tiles');
|
|
111
|
+
|
|
112
|
+
particleEffectSettingGroup = 'Texture';
|
|
113
|
+
particleEffectAddSetting('shape', 'shape', 'soft', 0, 0, 0,
|
|
114
|
+
'Built-in shape, none uses the tile');
|
|
115
|
+
particleEffectAddSetting('tileIndex', 'number', -1, -1, 63, 1,
|
|
116
|
+
'Tile in texture 0 when no shape, -1 is untextured', -1, 1e4);
|
|
117
|
+
particleEffectAddSetting('tileSize', 'number', 16, 1, 128, 1,
|
|
118
|
+
'Tile size in texture pixels', 1, 4096);
|
|
119
|
+
particleEffectAddSetting('tilePadding', 'number', 1, 0, 8, 1,
|
|
120
|
+
'Pixels of padding around each tile', 0, 64);
|
|
121
|
+
|
|
122
|
+
/** The behaviors an effect can use, each a push applied to every particle every update, with a 2D and a 3D version
|
|
123
|
+
* @type {Array<{name:string, update:(Function|undefined), update3D:(Function|undefined), min:number, max:number,
|
|
124
|
+
* value:number, description:string}>}
|
|
125
|
+
* @memberof ParticleEffects */
|
|
126
|
+
const particleEffectBehaviors =
|
|
127
|
+
[
|
|
128
|
+
{name:'wobble', min:0, max:5, value:1, description:'Sway from side to side'},
|
|
129
|
+
{name:'swirl', min:-2, max:2, value:1, description:'Turn the direction of travel'},
|
|
130
|
+
{name:'turbulence', min:0, max:5, value:1, description:'Random pushes every frame'},
|
|
131
|
+
{name:'attract', min:-2, max:2, value:1, description:'Pull toward the emitter, negative pushes away'},
|
|
132
|
+
{name:'orbit', min:-2, max:2, value:1, description:'Push around the emitter, add attract to circle it'},
|
|
133
|
+
{name:'wind', min:-2, max:2, value:1, description:'A sideways push that grows with age'},
|
|
134
|
+
{name:'stick', min:0, max:1, value:1, description:'Grip on landing, pair with collideLevel'},
|
|
135
|
+
].map(b=> ({...b, update: undefined, update3D: undefined}));
|
|
136
|
+
const particleEffectBehavior = (name)=> particleEffectBehaviors.find(b=> b.name === name);
|
|
137
|
+
|
|
138
|
+
/** Names of the shapes the plugin draws, for an effect's shape setting
|
|
139
|
+
* @type {Array<string>}
|
|
140
|
+
* @memberof ParticleEffects */
|
|
141
|
+
const particleEffectShapes = ['dot', 'soft', 'glow', 'smoke', 'spark', 'square', 'triangle', 'ring', 'star', 'plus'];
|
|
142
|
+
|
|
143
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
144
|
+
// effects
|
|
145
|
+
|
|
146
|
+
// a color from an [r,g,b,a] array or a Color, undefined if it is neither
|
|
147
|
+
function particleEffectColor(value)
|
|
148
|
+
{
|
|
149
|
+
if (value instanceof Color)
|
|
150
|
+
value = [value.r, value.g, value.b, value.a];
|
|
151
|
+
if (isArray(value) && value.length === 4 && value.every(isNumber))
|
|
152
|
+
return value.map(c=> clamp(c));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Any input into a whole effect: missing fields take their defaults, bad ones are dropped or clamped
|
|
156
|
+
* @param {Object} [raw] - {name, settings, behaviors}, from a library file, the designer or code
|
|
157
|
+
* @return {{name:string, settings:Object, behaviors:Array<{name:string, strength:number}>}}
|
|
158
|
+
* @memberof ParticleEffects */
|
|
159
|
+
function particleEffectSanitize(raw)
|
|
160
|
+
{
|
|
161
|
+
// one line of text, line breaks and control characters made spaces
|
|
162
|
+
const text = typeof raw?.name === 'string' ? raw.name.replace(/[\x00-\x1f\x7f]+/g, ' ').trim() : '';
|
|
163
|
+
const name = text ? text.slice(0, 60) : 'Effect';
|
|
164
|
+
const input = raw?.settings && typeof raw.settings === 'object' ? {...raw.settings} : {};
|
|
165
|
+
input.collideLevel ??= input.collideTiles; // its name before 1.20
|
|
166
|
+
// a library saved before shapes names its tile and no shape, it keeps its tile
|
|
167
|
+
if (input.shape === undefined && isNumber(input.tileIndex))
|
|
168
|
+
input.shape = '';
|
|
169
|
+
const settings = {};
|
|
170
|
+
for (const setting of particleEffectSettings)
|
|
171
|
+
{
|
|
172
|
+
const value = input[setting.name], fallback = setting.value;
|
|
173
|
+
if (setting.kind === 'checkbox')
|
|
174
|
+
settings[setting.name] = typeof value === 'boolean' ? value : value === 0 || value === 1 ? !!value : fallback;
|
|
175
|
+
else if (setting.kind === 'color')
|
|
176
|
+
settings[setting.name] = particleEffectColor(value) || fallback.slice();
|
|
177
|
+
else if (setting.kind === 'shape')
|
|
178
|
+
settings[setting.name] = value === '' || particleEffectShapes.includes(value) ? value : fallback;
|
|
179
|
+
else if (isNumber(value))
|
|
180
|
+
{
|
|
181
|
+
// an angle past half a turn comes around, a clamp would point it the other way
|
|
182
|
+
const wraps = setting.name === 'angle' && isFinite(value) && abs(value) > PI;
|
|
183
|
+
const clamped = clamp(wraps ? mod(value + PI, 2*PI) - PI : value, setting.hardMin, setting.hardMax);
|
|
184
|
+
settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
settings[setting.name] = fallback;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// local space particles are placed relative to the emitter, the tile collision is in the world
|
|
191
|
+
if (settings.localSpace)
|
|
192
|
+
settings.collideLevel = false;
|
|
193
|
+
|
|
194
|
+
// known behaviors once each, in table order
|
|
195
|
+
const behaviors = [];
|
|
196
|
+
const list = isArray(raw?.behaviors) ? raw.behaviors : [];
|
|
197
|
+
for (const b of particleEffectBehaviors)
|
|
198
|
+
{
|
|
199
|
+
const found = list.find(x=> x?.name === b.name);
|
|
200
|
+
if (!found)
|
|
201
|
+
continue;
|
|
202
|
+
const strength = isNumber(found.strength) ? clamp(found.strength, b.min, b.max) : b.value;
|
|
203
|
+
behaviors.push({name:b.name, strength});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// behaviors not known yet stay, a game may add them after the effect is loaded
|
|
207
|
+
for (const x of list)
|
|
208
|
+
if (typeof x?.name === 'string' && x.name && !particleEffectBehavior(x.name) &&
|
|
209
|
+
!behaviors.some(b=> b.name === x.name))
|
|
210
|
+
behaviors.push({name:x.name, strength: isNumber(x.strength) ? x.strength : 1});
|
|
211
|
+
return {name, settings, behaviors};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A copy of an effect with its four colors turned around the color wheel and their saturation scaled; lightness and
|
|
215
|
+
* alpha stay, and grey and white have no hue to turn
|
|
216
|
+
* @param {Object} effect
|
|
217
|
+
* @param {number} [hue] - How far around the wheel, 1 is all the way
|
|
218
|
+
* @param {number} [saturation] - Multiplies the saturation, 0 is grey, clamped to 1
|
|
219
|
+
* @return {Object}
|
|
220
|
+
* @memberof ParticleEffects */
|
|
221
|
+
function particleEffectRecolor(effect, hue=0, saturation=1)
|
|
222
|
+
{
|
|
223
|
+
effect = particleEffectSanitize(effect); // colors as channels, whatever it was given
|
|
224
|
+
const settings = {...effect.settings};
|
|
225
|
+
for (const name of ['colorStartA', 'colorStartB', 'colorEndA', 'colorEndB'])
|
|
226
|
+
{
|
|
227
|
+
const [h, s, l, a] = new Color(...settings[name]).HSLA();
|
|
228
|
+
const c = new Color().setHSLA(mod(h + hue), clamp(s * saturation), l, a);
|
|
229
|
+
settings[name] = [c.r, c.g, c.b, c.a];
|
|
230
|
+
}
|
|
231
|
+
return {...effect, settings, behaviors: effect.behaviors.map(b=> ({...b}))};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
235
|
+
// libraries
|
|
236
|
+
|
|
237
|
+
// the effects by lower case name, the built-ins and any a game adds or loads
|
|
238
|
+
const particleEffectsByName = new Map;
|
|
239
|
+
|
|
240
|
+
/** Add effects to play by name, sanitized; one with the name of an effect already there replaces it
|
|
241
|
+
* @param {Array<Object>|Object} effects
|
|
242
|
+
* @memberof ParticleEffects */
|
|
243
|
+
function particleEffectsAdd(effects)
|
|
244
|
+
{
|
|
245
|
+
for (const effect of isArray(effects) ? effects : [effects])
|
|
246
|
+
{
|
|
247
|
+
const clean = particleEffectSanitize(effect);
|
|
248
|
+
particleEffectsByName.set(clean.name.toLowerCase(), clean);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The effect with a name, any case, undefined if there is none
|
|
253
|
+
* @param {string} name
|
|
254
|
+
* @return {Object|undefined}
|
|
255
|
+
* @memberof ParticleEffects */
|
|
256
|
+
function particleEffectsGet(name) { return particleEffectsByName.get(String(name).toLowerCase()); }
|
|
257
|
+
|
|
258
|
+
/** A library file, or one effect, into sanitized effects; throws on bad input
|
|
259
|
+
* @param {string} text
|
|
260
|
+
* @return {Array<Object>}
|
|
261
|
+
* @memberof ParticleEffects */
|
|
262
|
+
function particleEffectsParse(text) { return particleEffectsFromData(JSON.parse(text)); }
|
|
263
|
+
|
|
264
|
+
// a library file's data, or one effect's, into sanitized effects
|
|
265
|
+
function particleEffectsFromData(data)
|
|
266
|
+
{
|
|
267
|
+
const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
|
|
268
|
+
if (!list || !list.length)
|
|
269
|
+
throw new Error('no effects in this file');
|
|
270
|
+
return list.map(particleEffectSanitize);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The text of a library file with these effects, what particleEffectsLoad and the designer read
|
|
274
|
+
* @param {Array<Object>} effects
|
|
275
|
+
* @return {string}
|
|
276
|
+
* @memberof ParticleEffects */
|
|
277
|
+
function particleEffectsText(effects) { return JSON.stringify({version:1, effects}, undefined, 1); }
|
|
278
|
+
|
|
279
|
+
/** Load a library file the particle designer saved and add its effects
|
|
280
|
+
* @param {string} url
|
|
281
|
+
* @return {Promise<Array<Object>>} - The effects it had
|
|
282
|
+
* @memberof ParticleEffects */
|
|
283
|
+
async function particleEffectsLoad(url)
|
|
284
|
+
{
|
|
285
|
+
const effects = particleEffectsFromData(await fetchJSON(url));
|
|
286
|
+
particleEffectsAdd(effects);
|
|
287
|
+
return effects;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
291
|
+
// the shape sheet: every shape white on clear in a cell of one canvas, drawn the first time one is asked for
|
|
292
|
+
|
|
293
|
+
let particleEffectShapeTiles;
|
|
294
|
+
|
|
295
|
+
/** The tile of a built-in shape, on a sheet the plugin draws once; undefined headless, without a canvas or without
|
|
296
|
+
* WebGL, where an effect draws untextured squares
|
|
297
|
+
* @param {string} name - One of particleEffectShapes
|
|
298
|
+
* @return {TileInfo|undefined}
|
|
299
|
+
* @memberof ParticleEffects */
|
|
300
|
+
function particleEffectShapeTile(name)
|
|
301
|
+
{
|
|
302
|
+
if (!particleEffectShapeTiles)
|
|
303
|
+
{
|
|
304
|
+
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
305
|
+
// drawn at 4 times 32 pixel cells, so a big soft shape stays smooth when textures are pixelated
|
|
306
|
+
const cell = 32, r = 15, count = particleEffectShapes.length, res = 4;
|
|
307
|
+
const context = createCanvasContext(cell * count * res, cell * res);
|
|
308
|
+
context.scale(res, res);
|
|
309
|
+
context.fillStyle = context.strokeStyle = '#fff';
|
|
310
|
+
const soft = (x, y, radius, alpha=(t)=> 1 - t)=>
|
|
311
|
+
{
|
|
312
|
+
// a round falloff from the middle out
|
|
313
|
+
const g = context.createRadialGradient(x, y, 0, x, y, radius);
|
|
314
|
+
for (let i = 0; i <= 8; ++i)
|
|
315
|
+
g.addColorStop(i/8, 'rgba(255,255,255,' + alpha(i/8).toFixed(3) + ')');
|
|
316
|
+
context.fillStyle = g;
|
|
317
|
+
context.beginPath(); context.arc(x, y, radius, 0, 2*PI); context.fill();
|
|
318
|
+
context.fillStyle = '#fff';
|
|
319
|
+
};
|
|
320
|
+
const draw =
|
|
321
|
+
{
|
|
322
|
+
dot: (x, y)=> { context.beginPath(); context.arc(x, y, r, 0, 2*PI); context.fill(); },
|
|
323
|
+
soft: (x, y)=> soft(x, y, r),
|
|
324
|
+
glow: (x, y)=> soft(x, y, r, (t)=> engineGlowAlpha(t, 1)),
|
|
325
|
+
smoke: (x, y)=>
|
|
326
|
+
{
|
|
327
|
+
// a lumpy puff of soft circles
|
|
328
|
+
for (const [dx, dy, dr] of [[-4,-3,9], [4,-2,8], [0,4,9], [-5,5,6], [6,5,6]])
|
|
329
|
+
soft(x + dx, y + dy, dr, (t)=> (1 - t) * .7);
|
|
330
|
+
},
|
|
331
|
+
spark: (x, y)=>
|
|
332
|
+
{
|
|
333
|
+
// a glow squeezed thin, a streak along the tile's up
|
|
334
|
+
context.save(); context.translate(x, y); context.scale(.3, 1);
|
|
335
|
+
soft(0, 0, r, (t)=> engineGlowAlpha(t, .6));
|
|
336
|
+
context.restore();
|
|
337
|
+
},
|
|
338
|
+
square: (x, y)=> context.fillRect(x - r, y - r, 2*r, 2*r),
|
|
339
|
+
triangle: (x, y)=>
|
|
340
|
+
{
|
|
341
|
+
context.beginPath(); context.moveTo(x, y - r); context.lineTo(x + r, y + r); context.lineTo(x - r, y + r);
|
|
342
|
+
context.fill();
|
|
343
|
+
},
|
|
344
|
+
ring: (x, y)=>
|
|
345
|
+
{
|
|
346
|
+
context.lineWidth = 3;
|
|
347
|
+
context.beginPath(); context.arc(x, y, r - 2, 0, 2*PI); context.stroke();
|
|
348
|
+
},
|
|
349
|
+
star: (x, y)=>
|
|
350
|
+
{
|
|
351
|
+
context.beginPath();
|
|
352
|
+
for (let i = 0; i < 10; ++i)
|
|
353
|
+
{
|
|
354
|
+
const a = i * PI / 5 - PI/2, d = i % 2 ? r * .45 : r;
|
|
355
|
+
context.lineTo(x + cos(a) * d, y + sin(a) * d);
|
|
356
|
+
}
|
|
357
|
+
context.fill();
|
|
358
|
+
},
|
|
359
|
+
plus: (x, y)=> { context.fillRect(x - r, y - 4, 2*r, 8); context.fillRect(x - 4, y - r, 8, 2*r); },
|
|
360
|
+
};
|
|
361
|
+
particleEffectShapes.forEach((name, i)=> draw[name](i * cell + cell/2, cell/2));
|
|
362
|
+
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
363
|
+
particleEffectShapeTiles = new Map(particleEffectShapes.map((name, i)=>
|
|
364
|
+
[name, new TileInfo(vec2((i * cell + 1) * res, res), vec2((cell - 2) * res), texture)]));
|
|
365
|
+
}
|
|
366
|
+
return particleEffectShapeTiles.get(name);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
370
|
+
// behaviors, the 2D pushes: a distance moved grows with the particle's scale, so a bigger effect moves the same way
|
|
371
|
+
|
|
372
|
+
{
|
|
373
|
+
const b = particleEffectBehavior;
|
|
374
|
+
b('wobble').update = (p, s)=>
|
|
375
|
+
{
|
|
376
|
+
// each particle on its own beat, from its randomized life, so nothing is added to it
|
|
377
|
+
p.velocity.x += s * .003 * p.scale * sin(time*6 + p.lifeTime * 97 % 9);
|
|
378
|
+
};
|
|
379
|
+
b('swirl').update = (p, s)=>
|
|
380
|
+
{
|
|
381
|
+
// turn the direction of travel a little each frame
|
|
382
|
+
const a = s * .05, c = cos(a), n = sin(a), v = p.velocity;
|
|
383
|
+
v.set(v.x*c - v.y*n, v.x*n + v.y*c);
|
|
384
|
+
};
|
|
385
|
+
b('turbulence').update = (p, s)=>
|
|
386
|
+
{
|
|
387
|
+
p.velocity.x += rand(-1, 1) * s * .005 * p.scale;
|
|
388
|
+
p.velocity.y += rand(-1, 1) * s * .005 * p.scale;
|
|
389
|
+
};
|
|
390
|
+
b('attract').update = (p, s)=>
|
|
391
|
+
{
|
|
392
|
+
// pull toward the emitter by the distance, which grows with the scale already
|
|
393
|
+
const e = p.emitter, x = e.localSpace ? 0 : e.pos.x, y = e.localSpace ? 0 : e.pos.y;
|
|
394
|
+
p.velocity.x += (x - p.pos.x) * s * .002;
|
|
395
|
+
p.velocity.y += (y - p.pos.y) * s * .002;
|
|
396
|
+
};
|
|
397
|
+
b('orbit').update = (p, s)=>
|
|
398
|
+
{
|
|
399
|
+
const e = p.emitter, x = e.localSpace ? 0 : e.pos.x, y = e.localSpace ? 0 : e.pos.y;
|
|
400
|
+
p.velocity.x -= (p.pos.y - y) * s * .002;
|
|
401
|
+
p.velocity.y += (p.pos.x - x) * s * .002;
|
|
402
|
+
};
|
|
403
|
+
b('wind').update = (p, s)=>
|
|
404
|
+
p.velocity.x += s * .004 * p.scale * min((time - p.spawnTime) / p.lifeTime, 1);
|
|
405
|
+
b('stick').update = (p, s)=>
|
|
406
|
+
{
|
|
407
|
+
// grip the ground on landing, pair with collideLevel
|
|
408
|
+
if (!p.groundObject) return;
|
|
409
|
+
p.velocity.x *= 1 - s;
|
|
410
|
+
p.angleVelocity *= 1 - s;
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Add a behavior effects can use by name, or replace one
|
|
415
|
+
* @param {string} name
|
|
416
|
+
* @param {function(Particle, number): void} update - Pushes a 2D particle, given the strength
|
|
417
|
+
* @param {function(Particle3D, number): void} [update3D] - The same for a 3D particle, none leaves 3D alone
|
|
418
|
+
* @param {number} [min] - Strength range the designer offers
|
|
419
|
+
* @param {number} [max]
|
|
420
|
+
* @param {number} [value] - Strength when first added
|
|
421
|
+
* @param {string} [description]
|
|
422
|
+
* @memberof ParticleEffects */
|
|
423
|
+
function particleEffectsAddBehavior(name, update, update3D, min=-2, max=2, value=1, description='')
|
|
424
|
+
{
|
|
425
|
+
const old = particleEffectBehavior(name);
|
|
426
|
+
old && particleEffectBehaviors.splice(particleEffectBehaviors.indexOf(old), 1);
|
|
427
|
+
particleEffectBehaviors.push({name, update, update3D, min, max, value, description});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// the update callback that runs an effect's behaviors, in 2D or 3D, undefined when it has none for that
|
|
431
|
+
function particleEffectUpdateCallback(behaviors, is3D)
|
|
432
|
+
{
|
|
433
|
+
const calls = behaviors.map(b=> [particleEffectBehavior(b.name)?.[is3D ? 'update3D' : 'update'], b.strength])
|
|
434
|
+
.filter(c=> c[0]);
|
|
435
|
+
if (calls.length)
|
|
436
|
+
return (p)=> { for (const [update, strength] of calls) update(p, strength); };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
440
|
+
// 2D effects
|
|
441
|
+
|
|
442
|
+
// the settings the builders set themselves instead of copying across
|
|
443
|
+
const particleEffectIndirect = ['emitSize', 'emitRect', 'emitHeight', 'tileIndex', 'tileSize', 'tilePadding', 'shape'];
|
|
444
|
+
|
|
445
|
+
// the tile an effect draws with: its shape, else its tile in texture 0 when that fits, else none
|
|
446
|
+
function particleEffectTileInfo(s)
|
|
447
|
+
{
|
|
448
|
+
if (s.shape)
|
|
449
|
+
return particleEffectShapeTile(s.shape);
|
|
450
|
+
if (s.tileIndex < 0) return;
|
|
451
|
+
const texture = textureInfos[0], cell = s.tileSize + s.tilePadding*2;
|
|
452
|
+
if (!texture || !texture.size.x) return;
|
|
453
|
+
if (s.tileIndex < floor(texture.size.x / cell) * floor(texture.size.y / cell))
|
|
454
|
+
return tile(s.tileIndex, s.tileSize, 0, s.tilePadding);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Set a 2D emitter to an effect, live, so a running one keeps its particles
|
|
458
|
+
* @param {ParticleEmitter} emitter
|
|
459
|
+
* @param {Object} effect
|
|
460
|
+
* @memberof ParticleEffects */
|
|
461
|
+
function particleEffectApply(emitter, effect)
|
|
462
|
+
{
|
|
463
|
+
// an effect written by hand or changed since it was added
|
|
464
|
+
particleEffectSet(emitter, particleEffectSanitize(effect));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// set a 2D emitter to an effect that is sanitized already, as a play's is
|
|
468
|
+
function particleEffectSet(emitter, effect)
|
|
469
|
+
{
|
|
470
|
+
const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
|
|
471
|
+
for (const setting of particleEffectSettings)
|
|
472
|
+
if (!particleEffectIndirect.includes(setting.name))
|
|
473
|
+
emitter[setting.name] = setting.kind === 'color' ? new Color(...s[setting.name]) : s[setting.name];
|
|
474
|
+
ended && (emitter.emitTime = -1);
|
|
475
|
+
emitter.emitCircle = !s.emitRect;
|
|
476
|
+
emitter.emitSize = vec2(s.emitSize, s.emitRect ? s.emitHeight : s.emitSize);
|
|
477
|
+
emitter.tileInfo = particleEffectTileInfo(s);
|
|
478
|
+
emitter.renderOrder = s.additive ? 1e9 : 0;
|
|
479
|
+
emitter.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// an effect from a name or an effect, recolored by the options
|
|
483
|
+
function particleEffectResolve(nameOrEffect, options)
|
|
484
|
+
{
|
|
485
|
+
const found = typeof nameOrEffect == 'string' ? particleEffectsGet(nameOrEffect) : nameOrEffect;
|
|
486
|
+
ASSERT(!!found, 'no particle effect named ' + nameOrEffect);
|
|
487
|
+
if (!found) return;
|
|
488
|
+
// any setting in the options replaces the effect's own for this play, emitTime for a burst or a loop
|
|
489
|
+
const settings = {...found.settings};
|
|
490
|
+
for (const setting of particleEffectSettings)
|
|
491
|
+
if (options[setting.name] !== undefined)
|
|
492
|
+
settings[setting.name] = options[setting.name];
|
|
493
|
+
// sanitized each time, an effect may be written by hand or changed after it was added
|
|
494
|
+
const effect = particleEffectSanitize({...found, settings}), {hue=0, saturation=1} = options;
|
|
495
|
+
return hue || saturation != 1 ? particleEffectRecolor(effect, hue, saturation) : effect;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Play an effect: a 2D emitter set to it, placed, scaled and recolored
|
|
499
|
+
* - A continuous effect (fire, a torch) goes until destroyed or given an emitTime, a one-shot ends itself
|
|
500
|
+
* - Any setting in the options replaces the effect's own for this play: {emitTime: .5} for a burst of a continuous
|
|
501
|
+
* effect, {emitTime: 0, emitRate: 30} to keep a one-shot going, or speed, particleTime and the rest
|
|
502
|
+
* - Attach it to an object with addChild to follow it
|
|
503
|
+
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
504
|
+
* @param {Vector2} [pos]
|
|
505
|
+
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
506
|
+
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
507
|
+
* hue turns its colors around the color wheel, 1 is all the way;
|
|
508
|
+
* saturation multiplies its saturation, 0 is grey;
|
|
509
|
+
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
510
|
+
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
511
|
+
* tinted by its colors, a whole texture drawn as one tile;
|
|
512
|
+
* and any effect setting by its name, emitTime, emitRate, speed and the rest, replaces the effect's own
|
|
513
|
+
* @return {ParticleEmitter|undefined} - undefined when there is no such effect
|
|
514
|
+
* @memberof ParticleEffects */
|
|
515
|
+
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
516
|
+
{
|
|
517
|
+
const effect = particleEffectResolve(nameOrEffect, options);
|
|
518
|
+
if (!effect) return;
|
|
519
|
+
const emitter = new ParticleEmitter(pos.copy());
|
|
520
|
+
particleEffectSet(emitter, effect);
|
|
521
|
+
emitter.scale = options.scale ?? 1;
|
|
522
|
+
if (options.angle !== undefined)
|
|
523
|
+
emitter.angle = options.angle;
|
|
524
|
+
const tileInfo = options.tileInfo; // the game's own art in place of the shape, a whole texture as one tile
|
|
525
|
+
if (tileInfo)
|
|
526
|
+
emitter.tileInfo = tileInfo instanceof TextureInfo ? new TileInfo(vec2(), tileInfo.size, tileInfo) : tileInfo;
|
|
527
|
+
return emitter;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** An effect with a 2D emitter's settings, to save, build again, or build in 3D with particleEffect3D; its tile is
|
|
531
|
+
* left out, since a hand made emitter's tile is its own texture and not one an effect can name, and so is its
|
|
532
|
+
* scale, which an effect does not keep: pass it again with options.scale
|
|
533
|
+
* @param {ParticleEmitter} emitter
|
|
534
|
+
* @param {string} [name]
|
|
535
|
+
* @return {Object}
|
|
536
|
+
* @memberof ParticleEffects */
|
|
537
|
+
function particleEffectFromEmitter(emitter, name='Effect')
|
|
538
|
+
{
|
|
539
|
+
const settings = {};
|
|
540
|
+
for (const setting of particleEffectSettings)
|
|
541
|
+
if (!particleEffectIndirect.includes(setting.name) && emitter[setting.name] !== undefined)
|
|
542
|
+
settings[setting.name] = emitter[setting.name]; // a Color is taken as its channels by the sanitizer
|
|
543
|
+
settings.emitRect = !emitter.emitCircle;
|
|
544
|
+
settings.emitSize = emitter.emitSize.x;
|
|
545
|
+
settings.emitHeight = emitter.emitSize.y;
|
|
546
|
+
settings.shape = '';
|
|
547
|
+
settings.tileIndex = -1;
|
|
548
|
+
return particleEffectSanitize({name, settings});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
552
|
+
// 3D effects
|
|
553
|
+
|
|
554
|
+
// the 3D pushes: a 3D particle is one view object the emitter reuses, so what differs per particle comes from its
|
|
555
|
+
// randomized lifetime, and distances grow with the emitter's scale
|
|
556
|
+
{
|
|
557
|
+
const b = particleEffectBehavior;
|
|
558
|
+
const center = (p)=> p.emitter.worldPos3D || p.emitter.pos3D; // where it was this update
|
|
559
|
+
b('wobble').update3D = (p, s)=> p.velocity.x += s * .003 * p.scale * sin(time*6 + p.lifeTime * 97 % 9);
|
|
560
|
+
b('swirl').update3D = (p, s)=>
|
|
561
|
+
{
|
|
562
|
+
const a = s * .05, c = cos(a), n = sin(a), v = p.velocity;
|
|
563
|
+
v.set(v.x*c - v.y*n, v.x*n + v.y*c, v.z);
|
|
564
|
+
};
|
|
565
|
+
b('turbulence').update3D = (p, s)=>
|
|
566
|
+
{
|
|
567
|
+
const k = s * .005 * p.scale;
|
|
568
|
+
p.velocity.x += rand(-1, 1) * k, p.velocity.y += rand(-1, 1) * k, p.velocity.z += rand(-1, 1) * k;
|
|
569
|
+
};
|
|
570
|
+
b('attract').update3D = (p, s)=>
|
|
571
|
+
{
|
|
572
|
+
const c = center(p);
|
|
573
|
+
p.velocity.x += (c.x - p.pos.x) * s * .002;
|
|
574
|
+
p.velocity.y += (c.y - p.pos.y) * s * .002;
|
|
575
|
+
p.velocity.z += (c.z - p.pos.z) * s * .002;
|
|
576
|
+
};
|
|
577
|
+
b('orbit').update3D = (p, s)=>
|
|
578
|
+
{
|
|
579
|
+
const c = center(p);
|
|
580
|
+
p.velocity.x -= (p.pos.y - c.y) * s * .002;
|
|
581
|
+
p.velocity.y += (p.pos.x - c.x) * s * .002;
|
|
582
|
+
};
|
|
583
|
+
b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
|
|
584
|
+
// stick has no 3D push: particleEffectApply3D makes it the emitter's stick, its grip on landing
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
|
|
588
|
+
* @param {ParticleEmitter3D} emitter
|
|
589
|
+
* @param {Object} effect
|
|
590
|
+
* @memberof ParticleEffects */
|
|
591
|
+
function particleEffectApply3D(emitter, effect)
|
|
592
|
+
{
|
|
593
|
+
// an effect written by hand or changed since it was added
|
|
594
|
+
particleEffectSet3D(emitter, particleEffectSanitize(effect));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// set a 3D emitter to an effect that is sanitized already, as a play's is
|
|
598
|
+
function particleEffectSet3D(emitter, effect)
|
|
599
|
+
{
|
|
600
|
+
const s = effect.settings, e = emitter;
|
|
601
|
+
// a circle is a sphere and a rectangle a box as deep as it is wide
|
|
602
|
+
e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
|
|
603
|
+
e.emitTime = e.emitTime < 0 ? -1 : s.emitTime; // destroyed, its last particles going, stays so
|
|
604
|
+
e.emitRate = s.emitRate, e.emitConeAngle = s.emitConeAngle;
|
|
605
|
+
e.tileInfo = particleEffectTileInfo(s);
|
|
606
|
+
for (const name of ['colorStartA', 'colorStartB', 'colorEndA', 'colorEndB'])
|
|
607
|
+
e[name] = new Color(...s[name]);
|
|
608
|
+
for (const name of ['particleTime', 'sizeStart', 'sizeEnd', 'speed', 'damping', 'gravity', 'fadeRate',
|
|
609
|
+
'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
|
|
610
|
+
e[name] = s[name];
|
|
611
|
+
e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
|
|
612
|
+
e.friction = s.friction;
|
|
613
|
+
e.stick = effect.behaviors.find(b=> b.name == 'stick')?.strength ?? 0; // in 3D the emitter grips on landing
|
|
614
|
+
// a 2D emitter at angle a shoots along (sin a, cos a), a z turn r takes up to (-sin r, cos r), so r is -a
|
|
615
|
+
e.rotation3D = vec3(0, 0, -s.angle);
|
|
616
|
+
e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Play an effect in 3D: a ParticleEmitter3D set to it, placed, scaled and recolored
|
|
620
|
+
* - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
|
|
621
|
+
* rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
|
|
622
|
+
* sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
|
|
623
|
+
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the emitter's
|
|
624
|
+
* stick, its grip where a particle lands
|
|
625
|
+
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
626
|
+
* @param {Vector3} [pos3D]
|
|
627
|
+
* @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
|
|
628
|
+
* about z, so 0 is up; flatten makes the spawn area flat across the way it emits
|
|
629
|
+
* @return {ParticleEmitter3D|undefined} - undefined when there is no such effect
|
|
630
|
+
* @memberof ParticleEffects */
|
|
631
|
+
function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
632
|
+
{
|
|
633
|
+
const effect = particleEffectResolve(nameOrEffect, options);
|
|
634
|
+
if (!effect) return;
|
|
635
|
+
const e = new ParticleEmitter3D(pos3D.copy());
|
|
636
|
+
particleEffectSet3D(e, effect);
|
|
637
|
+
if (options.angle !== undefined)
|
|
638
|
+
e.rotation3D = vec3(0, 0, -options.angle);
|
|
639
|
+
e.scale3D = vec3(options.scale ?? 1);
|
|
640
|
+
e.emitFlat = !!options.flatten; // a sphere a disc, a box a sheet, across the way it emits
|
|
641
|
+
if (options.tileInfo)
|
|
642
|
+
e.tileInfo = options.tileInfo;
|
|
643
|
+
return e;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
647
|
+
// the built-in effects, each tuned around a one unit emitter; one-shots have an emitTime and end themselves
|
|
648
|
+
|
|
649
|
+
/** The names of the built-in effects, in the order a gallery shows them
|
|
650
|
+
* @type {Array<string>}
|
|
651
|
+
* @memberof ParticleEffects */
|
|
652
|
+
const particleEffectsBuiltIn = [];
|
|
653
|
+
{
|
|
654
|
+
const add = (name, settings, behaviors=[])=>
|
|
655
|
+
{
|
|
656
|
+
particleEffectsBuiltIn.push(name);
|
|
657
|
+
particleEffectsAdd({name, settings, behaviors});
|
|
658
|
+
};
|
|
659
|
+
const clear = (h, s, l)=> hsl(h, s, l, 0);
|
|
660
|
+
|
|
661
|
+
// fire
|
|
662
|
+
add('fire', {emitRate:120, emitSize:.6, emitConeAngle:.3, shape:'glow', particleTime:.8, sizeStart:.7,
|
|
663
|
+
sizeEnd:.15, speed:.02, angleSpeed:.02, damping:.95, gravity:.003, fadeRate:.3, randomness:.3, additive:true,
|
|
664
|
+
colorStartA:hsl(.08,1,.6), colorStartB:hsl(.14,1,.65), colorEndA:clear(.02,1,.5), colorEndB:clear(0,1,.3)});
|
|
665
|
+
add('torch', {emitRate:60, emitSize:.25, emitConeAngle:.2, shape:'glow', particleTime:.6, sizeStart:.45,
|
|
666
|
+
sizeEnd:.1, speed:.015, angleSpeed:.02, damping:.95, gravity:.003, fadeRate:.3, randomness:.3, additive:true,
|
|
667
|
+
colorStartA:hsl(.09,1,.6), colorStartB:hsl(.13,1,.7), colorEndA:clear(.03,1,.5), colorEndB:clear(0,1,.35)});
|
|
668
|
+
add('smoke', {emitRate:25, emitSize:.5, emitConeAngle:.3, shape:'smoke', particleTime:3, sizeStart:.5,
|
|
669
|
+
sizeEnd:2, speed:.01, angleSpeed:.01, damping:.99, gravity:.0004, fadeRate:.5, randomness:.3,
|
|
670
|
+
colorStartA:hsl(0,0,.5,.5), colorStartB:hsl(0,0,.3,.5), colorEndA:clear(0,0,.2), colorEndB:clear(0,0,.4)},
|
|
671
|
+
[{name:'wobble', strength:.5}]);
|
|
672
|
+
add('steam', {emitRate:30, emitSize:.4, emitConeAngle:.2, shape:'smoke', particleTime:1.5, sizeStart:.3,
|
|
673
|
+
sizeEnd:1.5, speed:.02, damping:.98, gravity:.0008, fadeRate:.6, randomness:.3,
|
|
674
|
+
colorStartA:hsl(0,0,1,.4), colorStartB:hsl(.6,.2,.9,.3), colorEndA:clear(0,0,1), colorEndB:clear(.6,.2,.9)},
|
|
675
|
+
[{name:'wobble', strength:.3}]);
|
|
676
|
+
|
|
677
|
+
// impacts
|
|
678
|
+
add('explosion', {emitRate:600, emitTime:.1, emitSize:.5, shape:'glow', particleTime:.6, sizeStart:1,
|
|
679
|
+
sizeEnd:.1, speed:.1, damping:.9, fadeRate:.2, randomness:.5, additive:true,
|
|
680
|
+
colorStartA:hsl(.12,1,.65), colorStartB:hsl(.06,1,.55), colorEndA:clear(0,1,.5), colorEndB:clear(0,0,.3)});
|
|
681
|
+
add('sparks', {emitRate:100, emitSize:.2, shape:'soft', particleTime:.6, sizeStart:.08, sizeEnd:.04,
|
|
682
|
+
speed:.07, damping:.96, gravity:-.006, fadeRate:.1, randomness:.4, additive:true, trailScale:3,
|
|
683
|
+
colorStartA:hsl(.17,1,.9), colorStartB:hsl(.12,1,.65), colorEndA:clear(.07,1,.5), colorEndB:clear(.03,1,.5)});
|
|
684
|
+
add('hit', {emitRate:400, emitTime:.05, emitSize:.2, shape:'spark', particleTime:.35, sizeStart:.5,
|
|
685
|
+
sizeEnd:.1, speed:.08, damping:.85, fadeRate:.1, randomness:.4, additive:true, trailScale:2,
|
|
686
|
+
colorStartA:hsl(0,0,1), colorStartB:hsl(0,1,.7), colorEndA:clear(0,1,.5), colorEndB:clear(0,1,.4)});
|
|
687
|
+
add('dust', {emitRate:300, emitTime:.05, emitSize:1, emitRect:true, emitHeight:.1, emitConeAngle:1.2,
|
|
688
|
+
shape:'smoke', particleTime:.8, sizeStart:.3, sizeEnd:.8, speed:.04, damping:.9, gravity:.0005,
|
|
689
|
+
fadeRate:.5, randomness:.4, colorStartA:hsl(.1,.3,.6,.5), colorStartB:hsl(.08,.2,.5,.5),
|
|
690
|
+
colorEndA:clear(.1,.3,.6), colorEndB:clear(.08,.2,.5)});
|
|
691
|
+
add('debris', {emitRate:200, emitTime:.05, emitSize:.5, emitConeAngle:1, shape:'square', particleTime:1.5,
|
|
692
|
+
sizeStart:.15, sizeEnd:.1, speed:.07, angleSpeed:.2, damping:.99, gravity:-.008, fadeRate:.1,
|
|
693
|
+
randomness:.5, colorStartA:hsl(.08,.4,.35), colorStartB:hsl(0,0,.4), colorEndA:hsl(.08,.4,.3),
|
|
694
|
+
colorEndB:hsl(0,0,.35)});
|
|
695
|
+
|
|
696
|
+
// pickups and magic
|
|
697
|
+
add('sparkle', {emitRate:30, emitSize:1, shape:'star', particleTime:.8, sizeStart:.3, sizeEnd:0,
|
|
698
|
+
speed:.005, angleSpeed:.05, fadeRate:.5, randomness:.3, additive:true,
|
|
699
|
+
colorStartA:hsl(.14,1,.7), colorStartB:hsl(0,0,1), colorEndA:clear(.14,1,.7), colorEndB:clear(0,0,1)});
|
|
700
|
+
add('magic', {emitRate:60, emitSize:1, shape:'soft', particleTime:1.5, sizeStart:.25, sizeEnd:0,
|
|
701
|
+
speed:.015, fadeRate:.3, randomness:.3, additive:true, colorStartA:hsl(.55,1,.65),
|
|
702
|
+
colorStartB:hsl(.79,1,.65), colorEndA:clear(0,0,1), colorEndB:clear(0,0,1)}, [{name:'swirl', strength:.5}]);
|
|
703
|
+
add('heal', {emitRate:30, emitSize:.8, emitConeAngle:.2, shape:'plus', particleTime:1.2, sizeStart:.25,
|
|
704
|
+
sizeEnd:.1, speed:.015, angleSpeed:0, particleConeAngle:0, gravity:.0005, fadeRate:.4, randomness:.3,
|
|
705
|
+
additive:true, colorStartA:hsl(.33,1,.6), colorStartB:hsl(.3,1,.75), colorEndA:clear(.33,1,.6),
|
|
706
|
+
colorEndB:clear(.3,1,.75)});
|
|
707
|
+
add('poison', {emitRate:20, emitSize:.8, shape:'smoke', particleTime:2, sizeStart:.3, sizeEnd:1,
|
|
708
|
+
speed:.006, gravity:.0002, fadeRate:.6, randomness:.4, colorStartA:hsl(.3,.8,.45,.6),
|
|
709
|
+
colorStartB:hsl(.8,.6,.45,.6), colorEndA:clear(.3,.8,.4), colorEndB:clear(.8,.6,.4)},
|
|
710
|
+
[{name:'wobble', strength:.5}]);
|
|
711
|
+
add('portal', {emitRate:80, emitSize:1, shape:'soft', particleTime:1, sizeStart:.15, sizeEnd:.05,
|
|
712
|
+
speed:.02, fadeRate:.3, randomness:.3, additive:true, colorStartA:hsl(.75,1,.6),
|
|
713
|
+
colorStartB:hsl(.6,1,.6), colorEndA:clear(.75,1,.6), colorEndB:clear(.6,1,.6)},
|
|
714
|
+
[{name:'orbit', strength:1}, {name:'attract', strength:.5}]);
|
|
715
|
+
|
|
716
|
+
// weather and ambience, one unit across at scale 1, so scale them to the view
|
|
717
|
+
add('rain', {emitRate:30, emitSize:1, emitRect:true, emitHeight:.6, angle:PI, emitConeAngle:0,
|
|
718
|
+
shape:'soft', particleTime:.8, sizeStart:.02, sizeEnd:.02, speed:.03, gravity:-.001, fadeRate:.1,
|
|
719
|
+
randomness:.2, trailScale:5, colorStartA:hsl(.63,1,.8,.6), colorStartB:hsl(.63,1,.8,.4),
|
|
720
|
+
colorEndA:hsl(.63,1,.8,.6), colorEndB:hsl(.63,1,.8,.4)});
|
|
721
|
+
add('snow', {emitRate:6, emitSize:1, emitRect:true, emitHeight:.6, angle:PI, emitConeAngle:.3,
|
|
722
|
+
shape:'soft', particleTime:4, sizeStart:.05, sizeEnd:.05, speed:.004, damping:.98, gravity:-.0002,
|
|
723
|
+
fadeRate:.2, randomness:.3, colorStartA:hsl(0,0,1), colorStartB:hsl(.58,1,.9), colorEndA:hsl(0,0,1),
|
|
724
|
+
colorEndB:hsl(.58,1,.9)}, [{name:'wobble', strength:.15}]);
|
|
725
|
+
add('leaves', {emitRate:3, emitSize:1, emitRect:true, emitHeight:.2, angle:PI, emitConeAngle:.5,
|
|
726
|
+
shape:'triangle', particleTime:4, sizeStart:.12, sizeEnd:.12, speed:.003, angleSpeed:.05, damping:.98,
|
|
727
|
+
gravity:-.0001, fadeRate:.2, randomness:.4, randomColorLinear:false, colorStartA:hsl(.25,.7,.4),
|
|
728
|
+
colorStartB:hsl(.07,.8,.5), colorEndA:hsl(.08,.6,.35), colorEndB:hsl(.05,.7,.4)},
|
|
729
|
+
[{name:'wobble', strength:.15}, {name:'wind', strength:.05}]);
|
|
730
|
+
add('bubbles', {emitRate:8, emitSize:1, emitRect:true, emitHeight:.1, emitConeAngle:.2, shape:'ring',
|
|
731
|
+
particleTime:2, sizeStart:.1, sizeEnd:.2, speed:.01, gravity:.0003, fadeRate:.2, randomness:.4,
|
|
732
|
+
colorStartA:hsl(.55,.8,.8,.8), colorStartB:hsl(.5,.6,.9,.6), colorEndA:hsl(.55,.8,.8,.3),
|
|
733
|
+
colorEndB:hsl(.5,.6,.9,.2)}, [{name:'wobble', strength:.3}]);
|
|
734
|
+
add('fireflies', {emitRate:3, emitSize:1, shape:'glow', particleTime:3, sizeStart:.15, sizeEnd:.15,
|
|
735
|
+
speed:.003, damping:.99, fadeRate:.8, randomness:.3, additive:true, colorStartA:hsl(.2,1,.6),
|
|
736
|
+
colorStartB:hsl(.15,1,.7), colorEndA:hsl(.2,1,.6), colorEndB:hsl(.15,1,.7)},
|
|
737
|
+
[{name:'turbulence', strength:.3}]);
|
|
738
|
+
|
|
739
|
+
// trails and shots
|
|
740
|
+
add('trail', {emitRate:60, shape:'soft', particleTime:.5, sizeStart:.4, sizeEnd:0, speed:0, angleSpeed:0,
|
|
741
|
+
fadeRate:.1, randomness:.2, additive:true, colorStartA:hsl(.08,1,.6), colorStartB:hsl(.12,1,.65),
|
|
742
|
+
colorEndA:clear(.03,1,.5), colorEndB:clear(0,1,.4)});
|
|
743
|
+
add('muzzle', {emitRate:300, emitTime:.05, emitConeAngle:.3, shape:'glow', particleTime:.2,
|
|
744
|
+
sizeStart:.5, sizeEnd:.1, speed:.1, damping:.8, fadeRate:.1, randomness:.3, additive:true,
|
|
745
|
+
colorStartA:hsl(.14,1,.8), colorStartB:hsl(.1,1,.6), colorEndA:clear(.08,1,.5), colorEndB:clear(.05,1,.4)});
|
|
746
|
+
|
|
747
|
+
// other
|
|
748
|
+
add('blood', {emitRate:300, emitTime:.1, emitConeAngle:.8, shape:'dot', particleTime:3, sizeStart:.15,
|
|
749
|
+
sizeEnd:.1, speed:.09, gravity:-.01, fadeRate:.1, randomness:.4, collideLevel:true, restitution:.1,
|
|
750
|
+
colorStartA:hsl(0,1,.35), colorStartB:hsl(0,1,.2), colorEndA:hsl(0,1,.25), colorEndB:hsl(0,1,.15)},
|
|
751
|
+
[{name:'stick', strength:1}]);
|
|
752
|
+
add('confetti', {emitRate:200, emitTime:.1, emitSize:.2, emitConeAngle:.6, shape:'square', particleTime:3,
|
|
753
|
+
sizeStart:.1, sizeEnd:.1, speed:.09, angleSpeed:.15, damping:.96, gravity:-.004, fadeRate:.1,
|
|
754
|
+
randomness:.4, randomColorLinear:false, colorStartA:hsl(0,1,.5), colorStartB:hsl(.6,1,.6),
|
|
755
|
+
colorEndA:hsl(0,1,.5), colorEndB:hsl(.6,1,.6)}, [{name:'wobble', strength:.3}]);
|
|
756
|
+
add('splash', {emitRate:300, emitTime:.05, emitSize:.5, emitConeAngle:.6, shape:'soft', particleTime:.7,
|
|
757
|
+
sizeStart:.12, sizeEnd:.06, speed:.08, gravity:-.008, fadeRate:.2, randomness:.4,
|
|
758
|
+
colorStartA:hsl(.58,.8,.7,.8), colorStartB:hsl(.55,.6,.85,.8), colorEndA:hsl(.58,.8,.7,.2),
|
|
759
|
+
colorEndB:hsl(.55,.6,.85,.2)});
|
|
760
|
+
}
|