littlejsengine 1.21.0 → 1.22.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.
@@ -0,0 +1,745 @@
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
+ const clamped = clamp(value, setting.hardMin, setting.hardMax);
182
+ settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
183
+ }
184
+ else
185
+ settings[setting.name] = fallback;
186
+ }
187
+
188
+ // local space particles are placed relative to the emitter, the tile collision is in the world
189
+ if (settings.localSpace)
190
+ settings.collideLevel = false;
191
+
192
+ // known behaviors once each, in table order
193
+ const behaviors = [];
194
+ const list = isArray(raw?.behaviors) ? raw.behaviors : [];
195
+ for (const b of particleEffectBehaviors)
196
+ {
197
+ const found = list.find(x=> x?.name === b.name);
198
+ if (!found)
199
+ continue;
200
+ const strength = isNumber(found.strength) ? clamp(found.strength, b.min, b.max) : b.value;
201
+ behaviors.push({name:b.name, strength});
202
+ }
203
+
204
+ // behaviors not known yet stay, a game may add them after the effect is loaded
205
+ for (const x of list)
206
+ if (typeof x?.name === 'string' && x.name && !particleEffectBehavior(x.name) &&
207
+ !behaviors.some(b=> b.name === x.name))
208
+ behaviors.push({name:x.name, strength: isNumber(x.strength) ? x.strength : 1});
209
+ return {name, settings, behaviors};
210
+ }
211
+
212
+ /** A copy of an effect with its four colors turned around the color wheel and their saturation scaled; lightness and
213
+ * alpha stay, and grey and white have no hue to turn
214
+ * @param {Object} effect
215
+ * @param {number} [hue] - How far around the wheel, 1 is all the way
216
+ * @param {number} [saturation] - Multiplies the saturation, 0 is grey, clamped to 1
217
+ * @return {Object}
218
+ * @memberof ParticleEffects */
219
+ function particleEffectRecolor(effect, hue=0, saturation=1)
220
+ {
221
+ effect = particleEffectSanitize(effect); // colors as channels, whatever it was given
222
+ const settings = {...effect.settings};
223
+ for (const name of ['colorStartA', 'colorStartB', 'colorEndA', 'colorEndB'])
224
+ {
225
+ const [h, s, l, a] = new Color(...settings[name]).HSLA();
226
+ const c = new Color().setHSLA(mod(h + hue), clamp(s * saturation), l, a);
227
+ settings[name] = [c.r, c.g, c.b, c.a];
228
+ }
229
+ return {...effect, settings, behaviors: effect.behaviors.map(b=> ({...b}))};
230
+ }
231
+
232
+ ///////////////////////////////////////////////////////////////////////////////
233
+ // libraries
234
+
235
+ // the effects by lower case name, the built-ins and any a game adds or loads
236
+ const particleEffectsByName = new Map;
237
+
238
+ /** Add effects to play by name, sanitized; one with the name of an effect already there replaces it
239
+ * @param {Array<Object>|Object} effects
240
+ * @memberof ParticleEffects */
241
+ function particleEffectsAdd(effects)
242
+ {
243
+ for (const effect of isArray(effects) ? effects : [effects])
244
+ {
245
+ const clean = particleEffectSanitize(effect);
246
+ particleEffectsByName.set(clean.name.toLowerCase(), clean);
247
+ }
248
+ }
249
+
250
+ /** The effect with a name, any case, undefined if there is none
251
+ * @param {string} name
252
+ * @return {Object|undefined}
253
+ * @memberof ParticleEffects */
254
+ function particleEffectsGet(name) { return particleEffectsByName.get(String(name).toLowerCase()); }
255
+
256
+ /** A library file, or one effect, into sanitized effects; throws on bad input
257
+ * @param {string} text
258
+ * @return {Array<Object>}
259
+ * @memberof ParticleEffects */
260
+ function particleEffectsParse(text)
261
+ {
262
+ const data = JSON.parse(text);
263
+ const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
264
+ if (!list || !list.length)
265
+ throw new Error('no effects in this file');
266
+ return list.map(particleEffectSanitize);
267
+ }
268
+
269
+ /** The text of a library file with these effects, what particleEffectsLoad and the designer read
270
+ * @param {Array<Object>} effects
271
+ * @return {string}
272
+ * @memberof ParticleEffects */
273
+ function particleEffectsText(effects) { return JSON.stringify({version:1, effects}, undefined, 1); }
274
+
275
+ /** Load a library file the particle designer saved and add its effects
276
+ * @param {string} url
277
+ * @return {Promise<Array<Object>>} - The effects it had
278
+ * @memberof ParticleEffects */
279
+ async function particleEffectsLoad(url)
280
+ {
281
+ const effects = particleEffectsParse(JSON.stringify(await fetchJSON(url)));
282
+ particleEffectsAdd(effects);
283
+ return effects;
284
+ }
285
+
286
+ ///////////////////////////////////////////////////////////////////////////////
287
+ // the shape sheet: every shape white on clear in a cell of one canvas, drawn the first time one is asked for
288
+
289
+ let particleEffectShapeTiles;
290
+
291
+ /** The tile of a built-in shape, on a sheet the plugin draws once; undefined headless, without a canvas or without
292
+ * WebGL, where an effect draws untextured squares
293
+ * @param {string} name - One of particleEffectShapes
294
+ * @return {TileInfo|undefined}
295
+ * @memberof ParticleEffects */
296
+ function particleEffectShapeTile(name)
297
+ {
298
+ if (!particleEffectShapeTiles)
299
+ {
300
+ if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
301
+ // drawn at 4 times 32 pixel cells, so a big soft shape stays smooth when textures are pixelated
302
+ const cell = 32, r = 15, count = particleEffectShapes.length, res = 4;
303
+ const context = createCanvasContext(cell * count * res, cell * res);
304
+ context.scale(res, res);
305
+ context.fillStyle = context.strokeStyle = '#fff';
306
+ const soft = (x, y, radius, alpha=(t)=> 1 - t)=>
307
+ {
308
+ // a round falloff from the middle out
309
+ const g = context.createRadialGradient(x, y, 0, x, y, radius);
310
+ for (let i = 0; i <= 8; ++i)
311
+ g.addColorStop(i/8, 'rgba(255,255,255,' + alpha(i/8).toFixed(3) + ')');
312
+ context.fillStyle = g;
313
+ context.beginPath(); context.arc(x, y, radius, 0, 2*PI); context.fill();
314
+ context.fillStyle = '#fff';
315
+ };
316
+ const draw =
317
+ {
318
+ dot: (x, y)=> { context.beginPath(); context.arc(x, y, r, 0, 2*PI); context.fill(); },
319
+ soft: (x, y)=> soft(x, y, r),
320
+ glow: (x, y)=> soft(x, y, r, (t)=> engineGlowAlpha(t, 1)),
321
+ smoke: (x, y)=>
322
+ {
323
+ // a lumpy puff of soft circles
324
+ for (const [dx, dy, dr] of [[-4,-3,9], [4,-2,8], [0,4,9], [-5,5,6], [6,5,6]])
325
+ soft(x + dx, y + dy, dr, (t)=> (1 - t) * .7);
326
+ },
327
+ spark: (x, y)=>
328
+ {
329
+ // a glow squeezed thin, a streak along the tile's up
330
+ context.save(); context.translate(x, y); context.scale(.3, 1);
331
+ soft(0, 0, r, (t)=> engineGlowAlpha(t, .6));
332
+ context.restore();
333
+ },
334
+ square: (x, y)=> context.fillRect(x - r, y - r, 2*r, 2*r),
335
+ triangle: (x, y)=>
336
+ {
337
+ context.beginPath(); context.moveTo(x, y - r); context.lineTo(x + r, y + r); context.lineTo(x - r, y + r);
338
+ context.fill();
339
+ },
340
+ ring: (x, y)=>
341
+ {
342
+ context.lineWidth = 3;
343
+ context.beginPath(); context.arc(x, y, r - 2, 0, 2*PI); context.stroke();
344
+ },
345
+ star: (x, y)=>
346
+ {
347
+ context.beginPath();
348
+ for (let i = 0; i < 10; ++i)
349
+ {
350
+ const a = i * PI / 5 - PI/2, d = i % 2 ? r * .45 : r;
351
+ context.lineTo(x + cos(a) * d, y + sin(a) * d);
352
+ }
353
+ context.fill();
354
+ },
355
+ plus: (x, y)=> { context.fillRect(x - r, y - 4, 2*r, 8); context.fillRect(x - 4, y - r, 8, 2*r); },
356
+ };
357
+ particleEffectShapes.forEach((name, i)=> draw[name](i * cell + cell/2, cell/2));
358
+ const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
359
+ particleEffectShapeTiles = new Map(particleEffectShapes.map((name, i)=>
360
+ [name, new TileInfo(vec2((i * cell + 1) * res, res), vec2((cell - 2) * res), texture)]));
361
+ }
362
+ return particleEffectShapeTiles.get(name);
363
+ }
364
+
365
+ ///////////////////////////////////////////////////////////////////////////////
366
+ // behaviors, the 2D pushes: a distance moved grows with the particle's scale, so a bigger effect moves the same way
367
+
368
+ {
369
+ const b = particleEffectBehavior;
370
+ b('wobble').update = (p, s)=>
371
+ {
372
+ // each particle on its own beat, from its randomized life, so nothing is added to it
373
+ p.velocity.x += s * .003 * p.scale * sin(time*6 + p.lifeTime * 97 % 9);
374
+ };
375
+ b('swirl').update = (p, s)=>
376
+ {
377
+ // turn the direction of travel a little each frame
378
+ const a = s * .05, c = cos(a), n = sin(a), v = p.velocity;
379
+ v.set(v.x*c - v.y*n, v.x*n + v.y*c);
380
+ };
381
+ b('turbulence').update = (p, s)=>
382
+ {
383
+ p.velocity.x += rand(-1, 1) * s * .005 * p.scale;
384
+ p.velocity.y += rand(-1, 1) * s * .005 * p.scale;
385
+ };
386
+ b('attract').update = (p, s)=>
387
+ {
388
+ // pull toward the emitter by the distance, which grows with the scale already
389
+ const e = p.emitter, x = e.localSpace ? 0 : e.pos.x, y = e.localSpace ? 0 : e.pos.y;
390
+ p.velocity.x += (x - p.pos.x) * s * .002;
391
+ p.velocity.y += (y - p.pos.y) * s * .002;
392
+ };
393
+ b('orbit').update = (p, s)=>
394
+ {
395
+ const e = p.emitter, x = e.localSpace ? 0 : e.pos.x, y = e.localSpace ? 0 : e.pos.y;
396
+ p.velocity.x -= (p.pos.y - y) * s * .002;
397
+ p.velocity.y += (p.pos.x - x) * s * .002;
398
+ };
399
+ b('wind').update = (p, s)=>
400
+ p.velocity.x += s * .004 * p.scale * min((time - p.spawnTime) / p.lifeTime, 1);
401
+ b('stick').update = (p, s)=>
402
+ {
403
+ // grip the ground on landing, pair with collideLevel
404
+ if (!p.groundObject) return;
405
+ p.velocity.x *= 1 - s;
406
+ p.angleVelocity *= 1 - s;
407
+ };
408
+ }
409
+
410
+ /** Add a behavior effects can use by name, or replace one
411
+ * @param {string} name
412
+ * @param {function(Particle, number): void} update - Pushes a 2D particle, given the strength
413
+ * @param {function(Particle3D, number): void} [update3D] - The same for a 3D particle, none leaves 3D alone
414
+ * @param {number} [min] - Strength range the designer offers
415
+ * @param {number} [max]
416
+ * @param {number} [value] - Strength when first added
417
+ * @param {string} [description]
418
+ * @memberof ParticleEffects */
419
+ function particleEffectsAddBehavior(name, update, update3D, min=-2, max=2, value=1, description='')
420
+ {
421
+ const old = particleEffectBehavior(name);
422
+ old && particleEffectBehaviors.splice(particleEffectBehaviors.indexOf(old), 1);
423
+ particleEffectBehaviors.push({name, update, update3D, min, max, value, description});
424
+ }
425
+
426
+ // the update callback that runs an effect's behaviors, in 2D or 3D, undefined when it has none for that
427
+ function particleEffectUpdateCallback(behaviors, is3D)
428
+ {
429
+ const calls = behaviors.map(b=> [particleEffectBehavior(b.name)?.[is3D ? 'update3D' : 'update'], b.strength])
430
+ .filter(c=> c[0]);
431
+ if (calls.length)
432
+ return (p)=> { for (const [update, strength] of calls) update(p, strength); };
433
+ }
434
+
435
+ ///////////////////////////////////////////////////////////////////////////////
436
+ // 2D effects
437
+
438
+ // the settings the builders set themselves instead of copying across
439
+ const particleEffectIndirect = ['emitSize', 'emitRect', 'emitHeight', 'tileIndex', 'tileSize', 'tilePadding', 'shape'];
440
+
441
+ // the tile an effect draws with: its shape, else its tile in texture 0 when that fits, else none
442
+ function particleEffectTileInfo(s)
443
+ {
444
+ if (s.shape)
445
+ return particleEffectShapeTile(s.shape);
446
+ if (s.tileIndex < 0) return;
447
+ const texture = textureInfos[0], cell = s.tileSize + s.tilePadding*2;
448
+ if (!texture || !texture.size.x) return;
449
+ if (s.tileIndex < floor(texture.size.x / cell) * floor(texture.size.y / cell))
450
+ return tile(s.tileIndex, s.tileSize, 0, s.tilePadding);
451
+ }
452
+
453
+ /** Set a 2D emitter to an effect, live, so a running one keeps its particles
454
+ * @param {ParticleEmitter} emitter
455
+ * @param {Object} effect
456
+ * @memberof ParticleEffects */
457
+ function particleEffectApply(emitter, effect)
458
+ {
459
+ effect = particleEffectSanitize(effect); // an effect written by hand or changed since it was added
460
+ const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
461
+ for (const setting of particleEffectSettings)
462
+ if (!particleEffectIndirect.includes(setting.name))
463
+ emitter[setting.name] = setting.kind === 'color' ? new Color(...s[setting.name]) : s[setting.name];
464
+ ended && (emitter.emitTime = -1);
465
+ emitter.emitCircle = !s.emitRect;
466
+ emitter.emitSize = vec2(s.emitSize, s.emitRect ? s.emitHeight : s.emitSize);
467
+ emitter.tileInfo = particleEffectTileInfo(s);
468
+ emitter.renderOrder = s.additive ? 1e9 : 0;
469
+ emitter.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors);
470
+ }
471
+
472
+ // an effect from a name or an effect, recolored by the options
473
+ function particleEffectResolve(nameOrEffect, options)
474
+ {
475
+ const found = typeof nameOrEffect == 'string' ? particleEffectsGet(nameOrEffect) : nameOrEffect;
476
+ ASSERT(!!found, 'no particle effect named ' + nameOrEffect);
477
+ if (!found) return;
478
+ // any setting in the options replaces the effect's own for this play, emitTime for a burst or a loop
479
+ const settings = {...found.settings};
480
+ for (const setting of particleEffectSettings)
481
+ if (options[setting.name] !== undefined)
482
+ settings[setting.name] = options[setting.name];
483
+ // sanitized each time, an effect may be written by hand or changed after it was added
484
+ const effect = particleEffectSanitize({...found, settings}), {hue=0, saturation=1} = options;
485
+ return hue || saturation != 1 ? particleEffectRecolor(effect, hue, saturation) : effect;
486
+ }
487
+
488
+ /** Play an effect: a 2D emitter set to it, placed, scaled and recolored
489
+ * - A continuous effect (fire, a torch) goes until destroyed or given an emitTime, a one-shot ends itself
490
+ * - Any setting in the options replaces the effect's own for this play: {emitTime: .5} for a burst of a continuous
491
+ * effect, {emitTime: 0, emitRate: 30} to keep a one-shot going, or speed, particleTime and the rest
492
+ * - Attach it to an object with addChild to follow it
493
+ * @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
494
+ * @param {Vector2} [pos]
495
+ * @param {Object} [options]
496
+ * @param {number} [options.scale] - Grows the whole effect, the built-ins fit a one unit object at 1
497
+ * @param {number} [options.hue] - Turns its colors around the color wheel, 1 is all the way
498
+ * @param {number} [options.saturation] - Multiplies its saturation, 0 is grey
499
+ * @param {number} [options.angle] - Direction, 0 is up; the effect's own angle when not given
500
+ * @param {TileInfo|TextureInfo} [options.tileInfo] - The game's own art to draw with in place of the effect's shape,
501
+ * tinted by its colors; a whole texture draws as one tile
502
+ * @param {*} [options.settings] - Any effect setting by its name, emitTime, emitRate, speed and the rest, replacing
503
+ * the effect's own for this play
504
+ * @return {ParticleEmitter|undefined} - undefined when there is no such effect
505
+ * @memberof ParticleEffects */
506
+ function particleEffect(nameOrEffect, pos=vec2(), options={})
507
+ {
508
+ const effect = particleEffectResolve(nameOrEffect, options);
509
+ if (!effect) return;
510
+ const emitter = new ParticleEmitter(pos.copy());
511
+ particleEffectApply(emitter, effect);
512
+ emitter.scale = options.scale ?? 1;
513
+ if (options.angle !== undefined)
514
+ emitter.angle = options.angle;
515
+ const tileInfo = options.tileInfo; // the game's own art in place of the shape, a whole texture as one tile
516
+ if (tileInfo)
517
+ emitter.tileInfo = tileInfo instanceof TextureInfo ? new TileInfo(vec2(), tileInfo.size, tileInfo) : tileInfo;
518
+ return emitter;
519
+ }
520
+
521
+ /** An effect with a 2D emitter's settings, to save, build again, or build in 3D with particleEffect3D; its tile is
522
+ * left out, since a hand made emitter's tile is its own texture and not one an effect can name, and so is its
523
+ * scale, which an effect does not keep: pass it again with options.scale
524
+ * @param {ParticleEmitter} emitter
525
+ * @param {string} [name]
526
+ * @return {Object}
527
+ * @memberof ParticleEffects */
528
+ function particleEffectFromEmitter(emitter, name='Effect')
529
+ {
530
+ const settings = {};
531
+ for (const setting of particleEffectSettings)
532
+ if (!particleEffectIndirect.includes(setting.name) && emitter[setting.name] !== undefined)
533
+ settings[setting.name] = emitter[setting.name]; // a Color is taken as its channels by the sanitizer
534
+ settings.emitRect = !emitter.emitCircle;
535
+ settings.emitSize = emitter.emitSize.x;
536
+ settings.emitHeight = emitter.emitSize.y;
537
+ settings.shape = '';
538
+ settings.tileIndex = -1;
539
+ return particleEffectSanitize({name, settings});
540
+ }
541
+
542
+ ///////////////////////////////////////////////////////////////////////////////
543
+ // 3D effects
544
+
545
+ // the 3D pushes: a 3D particle is one view object the emitter reuses, so what differs per particle comes from its
546
+ // randomized lifetime, and distances grow with the emitter's scale
547
+ {
548
+ const b = particleEffectBehavior;
549
+ const center = (p)=> p.emitter.worldPos3D || p.emitter.pos3D; // where it was this update
550
+ b('wobble').update3D = (p, s)=> p.velocity.x += s * .003 * p.scale * sin(time*6 + p.lifeTime * 97 % 9);
551
+ b('swirl').update3D = (p, s)=>
552
+ {
553
+ const a = s * .05, c = cos(a), n = sin(a), v = p.velocity;
554
+ v.set(v.x*c - v.y*n, v.x*n + v.y*c, v.z);
555
+ };
556
+ b('turbulence').update3D = (p, s)=>
557
+ {
558
+ const k = s * .005 * p.scale;
559
+ p.velocity.x += rand(-1, 1) * k, p.velocity.y += rand(-1, 1) * k, p.velocity.z += rand(-1, 1) * k;
560
+ };
561
+ b('attract').update3D = (p, s)=>
562
+ {
563
+ const c = center(p);
564
+ p.velocity.x += (c.x - p.pos.x) * s * .002;
565
+ p.velocity.y += (c.y - p.pos.y) * s * .002;
566
+ p.velocity.z += (c.z - p.pos.z) * s * .002;
567
+ };
568
+ b('orbit').update3D = (p, s)=>
569
+ {
570
+ const c = center(p);
571
+ p.velocity.x -= (p.pos.y - c.y) * s * .002;
572
+ p.velocity.y += (p.pos.x - c.x) * s * .002;
573
+ };
574
+ b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
575
+ // stick has no 3D push: particleEffect3D makes it the friction a particle lands with
576
+ }
577
+
578
+ /** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
579
+ * @param {ParticleEmitter3D} emitter
580
+ * @param {Object} effect
581
+ * @memberof ParticleEffects */
582
+ function particleEffectApply3D(emitter, effect)
583
+ {
584
+ effect = particleEffectSanitize(effect); // an effect written by hand or changed since it was added
585
+ const s = effect.settings, e = emitter;
586
+ // a circle is a sphere and a rectangle a box as deep as it is wide
587
+ e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
588
+ e.emitTime = e.emitTime < 0 ? -1 : s.emitTime; // destroyed, its last particles going, stays so
589
+ e.emitRate = s.emitRate, e.emitConeAngle = s.emitConeAngle;
590
+ e.tileInfo = particleEffectTileInfo(s);
591
+ for (const name of ['colorStartA', 'colorStartB', 'colorEndA', 'colorEndB'])
592
+ e[name] = new Color(...s[name]);
593
+ for (const name of ['particleTime', 'sizeStart', 'sizeEnd', 'speed', 'damping', 'gravity', 'fadeRate',
594
+ 'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
595
+ e[name] = s[name];
596
+ e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
597
+ const stick = effect.behaviors.find(b=> b.name == 'stick'); // in 3D, a grip on landing is less sliding
598
+ e.friction = stick ? min(s.friction, 1 - stick.strength) : s.friction;
599
+ // 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
600
+ e.rotation3D = vec3(0, 0, -s.angle);
601
+ e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
602
+ }
603
+
604
+ /** Play an effect in 3D: a ParticleEmitter3D set to it, placed, scaled and recolored
605
+ * - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
606
+ * rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
607
+ * sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
608
+ * randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the friction a
609
+ * particle lands with
610
+ * @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
611
+ * @param {Vector3} [pos3D]
612
+ * @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
613
+ * about z, so 0 is up; flatten makes the spawn area flat across the way it emits
614
+ * @return {ParticleEmitter3D|undefined} - undefined when there is no such effect
615
+ * @memberof ParticleEffects */
616
+ function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
617
+ {
618
+ const effect = particleEffectResolve(nameOrEffect, options);
619
+ if (!effect) return;
620
+ const e = new ParticleEmitter3D(pos3D.copy());
621
+ particleEffectApply3D(e, effect);
622
+ if (options.angle !== undefined)
623
+ e.rotation3D = vec3(0, 0, -options.angle);
624
+ e.scale3D = vec3(options.scale ?? 1);
625
+ e.emitFlat = !!options.flatten; // a sphere a disc, a box a sheet, across the way it emits
626
+ if (options.tileInfo)
627
+ e.tileInfo = options.tileInfo;
628
+ return e;
629
+ }
630
+
631
+ ///////////////////////////////////////////////////////////////////////////////
632
+ // the built-in effects, each tuned around a one unit emitter; one-shots have an emitTime and end themselves
633
+
634
+ /** The names of the built-in effects, in the order a gallery shows them
635
+ * @type {Array<string>}
636
+ * @memberof ParticleEffects */
637
+ const particleEffectsBuiltIn = [];
638
+ {
639
+ const add = (name, settings, behaviors=[])=>
640
+ {
641
+ particleEffectsBuiltIn.push(name);
642
+ particleEffectsAdd({name, settings, behaviors});
643
+ };
644
+ const clear = (h, s, l)=> hsl(h, s, l, 0);
645
+
646
+ // fire
647
+ add('fire', {emitRate:120, emitSize:.6, emitConeAngle:.3, shape:'glow', particleTime:.8, sizeStart:.7,
648
+ sizeEnd:.15, speed:.02, angleSpeed:.02, damping:.95, gravity:.003, fadeRate:.3, randomness:.3, additive:true,
649
+ colorStartA:hsl(.08,1,.6), colorStartB:hsl(.14,1,.65), colorEndA:clear(.02,1,.5), colorEndB:clear(0,1,.3)});
650
+ add('torch', {emitRate:60, emitSize:.25, emitConeAngle:.2, shape:'glow', particleTime:.6, sizeStart:.45,
651
+ sizeEnd:.1, speed:.015, angleSpeed:.02, damping:.95, gravity:.003, fadeRate:.3, randomness:.3, additive:true,
652
+ colorStartA:hsl(.09,1,.6), colorStartB:hsl(.13,1,.7), colorEndA:clear(.03,1,.5), colorEndB:clear(0,1,.35)});
653
+ add('smoke', {emitRate:25, emitSize:.5, emitConeAngle:.3, shape:'smoke', particleTime:3, sizeStart:.5,
654
+ sizeEnd:2, speed:.01, angleSpeed:.01, damping:.99, gravity:.0004, fadeRate:.5, randomness:.3,
655
+ colorStartA:hsl(0,0,.5,.5), colorStartB:hsl(0,0,.3,.5), colorEndA:clear(0,0,.2), colorEndB:clear(0,0,.4)},
656
+ [{name:'wobble', strength:.5}]);
657
+ add('steam', {emitRate:30, emitSize:.4, emitConeAngle:.2, shape:'smoke', particleTime:1.5, sizeStart:.3,
658
+ sizeEnd:1.5, speed:.02, damping:.98, gravity:.0008, fadeRate:.6, randomness:.3,
659
+ colorStartA:hsl(0,0,1,.4), colorStartB:hsl(.6,.2,.9,.3), colorEndA:clear(0,0,1), colorEndB:clear(.6,.2,.9)},
660
+ [{name:'wobble', strength:.3}]);
661
+
662
+ // impacts
663
+ add('explosion', {emitRate:600, emitTime:.1, emitSize:.5, shape:'glow', particleTime:.6, sizeStart:1,
664
+ sizeEnd:.1, speed:.1, damping:.9, fadeRate:.2, randomness:.5, additive:true,
665
+ colorStartA:hsl(.12,1,.65), colorStartB:hsl(.06,1,.55), colorEndA:clear(0,1,.5), colorEndB:clear(0,0,.3)});
666
+ add('sparks', {emitRate:100, emitSize:.2, shape:'soft', particleTime:.6, sizeStart:.08, sizeEnd:.04,
667
+ speed:.07, damping:.96, gravity:-.006, fadeRate:.1, randomness:.4, additive:true, trailScale:3,
668
+ colorStartA:hsl(.17,1,.9), colorStartB:hsl(.12,1,.65), colorEndA:clear(.07,1,.5), colorEndB:clear(.03,1,.5)});
669
+ add('hit', {emitRate:400, emitTime:.05, emitSize:.2, shape:'spark', particleTime:.35, sizeStart:.5,
670
+ sizeEnd:.1, speed:.08, damping:.85, fadeRate:.1, randomness:.4, additive:true, trailScale:2,
671
+ colorStartA:hsl(0,0,1), colorStartB:hsl(0,1,.7), colorEndA:clear(0,1,.5), colorEndB:clear(0,1,.4)});
672
+ add('dust', {emitRate:300, emitTime:.05, emitSize:1, emitRect:true, emitHeight:.1, emitConeAngle:1.2,
673
+ shape:'smoke', particleTime:.8, sizeStart:.3, sizeEnd:.8, speed:.04, damping:.9, gravity:.0005,
674
+ fadeRate:.5, randomness:.4, colorStartA:hsl(.1,.3,.6,.5), colorStartB:hsl(.08,.2,.5,.5),
675
+ colorEndA:clear(.1,.3,.6), colorEndB:clear(.08,.2,.5)});
676
+ add('debris', {emitRate:200, emitTime:.05, emitSize:.5, emitConeAngle:1, shape:'square', particleTime:1.5,
677
+ sizeStart:.15, sizeEnd:.1, speed:.07, angleSpeed:.2, damping:.99, gravity:-.008, fadeRate:.1,
678
+ randomness:.5, colorStartA:hsl(.08,.4,.35), colorStartB:hsl(0,0,.4), colorEndA:hsl(.08,.4,.3),
679
+ colorEndB:hsl(0,0,.35)});
680
+
681
+ // pickups and magic
682
+ add('sparkle', {emitRate:30, emitSize:1, shape:'star', particleTime:.8, sizeStart:.3, sizeEnd:0,
683
+ speed:.005, angleSpeed:.05, fadeRate:.5, randomness:.3, additive:true,
684
+ colorStartA:hsl(.14,1,.7), colorStartB:hsl(0,0,1), colorEndA:clear(.14,1,.7), colorEndB:clear(0,0,1)});
685
+ add('magic', {emitRate:60, emitSize:1, shape:'soft', particleTime:1.5, sizeStart:.25, sizeEnd:0,
686
+ speed:.015, fadeRate:.3, randomness:.3, additive:true, colorStartA:hsl(.55,1,.65),
687
+ colorStartB:hsl(.79,1,.65), colorEndA:clear(0,0,1), colorEndB:clear(0,0,1)}, [{name:'swirl', strength:.5}]);
688
+ add('heal', {emitRate:30, emitSize:.8, emitConeAngle:.2, shape:'plus', particleTime:1.2, sizeStart:.25,
689
+ sizeEnd:.1, speed:.015, angleSpeed:0, particleConeAngle:0, gravity:.0005, fadeRate:.4, randomness:.3,
690
+ additive:true, colorStartA:hsl(.33,1,.6), colorStartB:hsl(.3,1,.75), colorEndA:clear(.33,1,.6),
691
+ colorEndB:clear(.3,1,.75)});
692
+ add('poison', {emitRate:20, emitSize:.8, shape:'smoke', particleTime:2, sizeStart:.3, sizeEnd:1,
693
+ speed:.006, gravity:.0002, fadeRate:.6, randomness:.4, colorStartA:hsl(.3,.8,.45,.6),
694
+ colorStartB:hsl(.8,.6,.45,.6), colorEndA:clear(.3,.8,.4), colorEndB:clear(.8,.6,.4)},
695
+ [{name:'wobble', strength:.5}]);
696
+ add('portal', {emitRate:80, emitSize:1, shape:'soft', particleTime:1, sizeStart:.15, sizeEnd:.05,
697
+ speed:.02, fadeRate:.3, randomness:.3, additive:true, colorStartA:hsl(.75,1,.6),
698
+ colorStartB:hsl(.6,1,.6), colorEndA:clear(.75,1,.6), colorEndB:clear(.6,1,.6)},
699
+ [{name:'orbit', strength:1}, {name:'attract', strength:.5}]);
700
+
701
+ // weather and ambience, one unit across at scale 1, so scale them to the view
702
+ add('rain', {emitRate:30, emitSize:1, emitRect:true, emitHeight:.6, angle:PI, emitConeAngle:0,
703
+ shape:'soft', particleTime:.8, sizeStart:.02, sizeEnd:.02, speed:.03, gravity:-.001, fadeRate:.1,
704
+ randomness:.2, trailScale:5, colorStartA:hsl(.63,1,.8,.6), colorStartB:hsl(.63,1,.8,.4),
705
+ colorEndA:hsl(.63,1,.8,.6), colorEndB:hsl(.63,1,.8,.4)});
706
+ add('snow', {emitRate:6, emitSize:1, emitRect:true, emitHeight:.6, angle:PI, emitConeAngle:.3,
707
+ shape:'soft', particleTime:4, sizeStart:.05, sizeEnd:.05, speed:.004, damping:.98, gravity:-.0002,
708
+ fadeRate:.2, randomness:.3, colorStartA:hsl(0,0,1), colorStartB:hsl(.58,1,.9), colorEndA:hsl(0,0,1),
709
+ colorEndB:hsl(.58,1,.9)}, [{name:'wobble', strength:.15}]);
710
+ add('leaves', {emitRate:3, emitSize:1, emitRect:true, emitHeight:.2, angle:PI, emitConeAngle:.5,
711
+ shape:'triangle', particleTime:4, sizeStart:.12, sizeEnd:.12, speed:.003, angleSpeed:.05, damping:.98,
712
+ gravity:-.0001, fadeRate:.2, randomness:.4, randomColorLinear:false, colorStartA:hsl(.25,.7,.4),
713
+ colorStartB:hsl(.07,.8,.5), colorEndA:hsl(.08,.6,.35), colorEndB:hsl(.05,.7,.4)},
714
+ [{name:'wobble', strength:.15}, {name:'wind', strength:.05}]);
715
+ add('bubbles', {emitRate:8, emitSize:1, emitRect:true, emitHeight:.1, emitConeAngle:.2, shape:'ring',
716
+ particleTime:2, sizeStart:.1, sizeEnd:.2, speed:.01, gravity:.0003, fadeRate:.2, randomness:.4,
717
+ colorStartA:hsl(.55,.8,.8,.8), colorStartB:hsl(.5,.6,.9,.6), colorEndA:hsl(.55,.8,.8,.3),
718
+ colorEndB:hsl(.5,.6,.9,.2)}, [{name:'wobble', strength:.3}]);
719
+ add('fireflies', {emitRate:3, emitSize:1, shape:'glow', particleTime:3, sizeStart:.15, sizeEnd:.15,
720
+ speed:.003, damping:.99, fadeRate:.8, randomness:.3, additive:true, colorStartA:hsl(.2,1,.6),
721
+ colorStartB:hsl(.15,1,.7), colorEndA:hsl(.2,1,.6), colorEndB:hsl(.15,1,.7)},
722
+ [{name:'turbulence', strength:.3}]);
723
+
724
+ // trails and shots
725
+ add('trail', {emitRate:60, shape:'soft', particleTime:.5, sizeStart:.4, sizeEnd:0, speed:0, angleSpeed:0,
726
+ fadeRate:.1, randomness:.2, additive:true, colorStartA:hsl(.08,1,.6), colorStartB:hsl(.12,1,.65),
727
+ colorEndA:clear(.03,1,.5), colorEndB:clear(0,1,.4)});
728
+ add('muzzle', {emitRate:300, emitTime:.05, emitConeAngle:.3, shape:'glow', particleTime:.2,
729
+ sizeStart:.5, sizeEnd:.1, speed:.1, damping:.8, fadeRate:.1, randomness:.3, additive:true,
730
+ colorStartA:hsl(.14,1,.8), colorStartB:hsl(.1,1,.6), colorEndA:clear(.08,1,.5), colorEndB:clear(.05,1,.4)});
731
+
732
+ // other
733
+ add('blood', {emitRate:300, emitTime:.1, emitConeAngle:.8, shape:'dot', particleTime:3, sizeStart:.15,
734
+ sizeEnd:.1, speed:.09, gravity:-.01, fadeRate:.1, randomness:.4, collideLevel:true, restitution:.1,
735
+ colorStartA:hsl(0,1,.35), colorStartB:hsl(0,1,.2), colorEndA:hsl(0,1,.25), colorEndB:hsl(0,1,.15)},
736
+ [{name:'stick', strength:1}]);
737
+ add('confetti', {emitRate:200, emitTime:.1, emitSize:.2, emitConeAngle:.6, shape:'square', particleTime:3,
738
+ sizeStart:.1, sizeEnd:.1, speed:.09, angleSpeed:.15, damping:.96, gravity:-.004, fadeRate:.1,
739
+ randomness:.4, randomColorLinear:false, colorStartA:hsl(0,1,.5), colorStartB:hsl(.6,1,.6),
740
+ colorEndA:hsl(0,1,.5), colorEndB:hsl(.6,1,.6)}, [{name:'wobble', strength:.3}]);
741
+ add('splash', {emitRate:300, emitTime:.05, emitSize:.5, emitConeAngle:.6, shape:'soft', particleTime:.7,
742
+ sizeStart:.12, sizeEnd:.06, speed:.08, gravity:-.008, fadeRate:.2, randomness:.4,
743
+ colorStartA:hsl(.58,.8,.7,.8), colorStartB:hsl(.55,.6,.85,.8), colorEndA:hsl(.58,.8,.7,.2),
744
+ colorEndB:hsl(.55,.6,.85,.2)});
745
+ }