littlejsengine 1.19.3 → 1.21.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.
Files changed (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
@@ -1,533 +1,584 @@
1
- /**
2
- * LittleJS Particle System
3
- * - Fast and flexible particle effects system
4
- * - ParticleEmitter spawns and manages lightweight Particle objects
5
- * - Particles support color gradients, fading, rotation, and scaling
6
- * - Physics simulation with velocity, gravity, and damping
7
- * - Collision detection with tile layers
8
- * - Additive blending for glowing effects
9
- * - Cone-based emission with randomization
10
- * - Particle design tool available for easy emitter creation
11
- * @namespace Particles
12
- */
13
-
14
- 'use strict';
15
-
16
- /**
17
- * @callback ParticleCallback - Function that processes a particle
18
- * @param {Particle} particle
19
- * @memberof Particles
20
- */
21
-
22
- /**
23
- * @callback ParticleCollideCallback - Collide callback for particles
24
- * @param {Particle} particle
25
- * @param {number} tileData
26
- * @param {Vector2} pos
27
- * @memberof Particles
28
- */
29
-
30
- /**
31
- * Particle Emitter - Spawns particles with the given settings
32
- * @extends EngineObject
33
- * @memberof Particles
34
- * @example
35
- * // create a particle emitter
36
- * let pos = vec2(2,3);
37
- * let particleEmitter = new ParticleEmitter
38
- * (
39
- * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
40
- * tile(0, 16), // tileInfo
41
- * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
42
- * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
43
- * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
44
- * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
45
- * .5, 1 // randomness, collide
46
- * );
47
- */
48
- class ParticleEmitter extends EngineObject
49
- {
50
- /** Create a particle system with the given settings
51
- * @param {Vector2} pos - World space position of the emitter
52
- * @param {number} [angle] - Angle to emit the particles
53
- * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
54
- * @param {number} [emitTime] - How long to stay alive (0 is forever)
55
- * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
56
- * @param {number} [emitConeAngle=PI] - Local angle to apply velocity to particles from emitter
57
- * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
58
- * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
59
- * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
60
- * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
61
- * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
62
- * @param {number} [particleTime] - How long particles live
63
- * @param {number} [sizeStart] - How big are particles at start
64
- * @param {number} [sizeEnd] - How big are particles at end
65
- * @param {number} [speed] - How fast are particles when spawned, in world units per frame (at 60fps, so multiply units/sec by 1/60)
66
- * @param {number} [angleSpeed] - How fast are particles rotating, in radians per frame (at 60fps)
67
- * @param {number} [damping] - How much to dampen particle speed, per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame)
68
- * @param {number} [angleDamping] - How much to dampen particle angular speed, per-frame multiplier (1 = no damping)
69
- * @param {number} [gravityScale] - How much gravity effect particles
70
- * @param {number} [particleConeAngle] - Cone for start particle angle
71
- * @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
72
- * @param {number} [randomness] - Apply extra randomness percent
73
- * @param {boolean} [collideTiles] - Do particles collide against tiles
74
- * @param {boolean} [additive] - Should particles use additive blend
75
- * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
76
- * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
77
- * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
78
- */
79
- constructor
80
- (
81
- pos,
82
- angle,
83
- emitSize = 0,
84
- emitTime = 0,
85
- emitRate = 100,
86
- emitConeAngle = PI,
87
- tileInfo,
88
- colorStartA = WHITE,
89
- colorStartB = WHITE,
90
- colorEndA = CLEAR_WHITE,
91
- colorEndB = CLEAR_WHITE,
92
- particleTime = .5,
93
- sizeStart = .1,
94
- sizeEnd = 1,
95
- speed = .1,
96
- angleSpeed = .05,
97
- damping = 1,
98
- angleDamping = 1,
99
- gravityScale = 0,
100
- particleConeAngle = PI,
101
- fadeRate = .1,
102
- randomness = .2,
103
- collideTiles = false,
104
- additive = false,
105
- randomColorLinear = true,
106
- renderOrder = additive ? 1e9 : 0,
107
- localSpace = false
108
- )
109
- {
110
- super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
111
-
112
- // emitter settings
113
- /** @property {boolean} - Should particles be emitted in a circle */
114
- this.emitCircle = typeof emitSize === 'number';
115
- /** @property {number|Vector2} - World space size of the emitter (float for circle diameter, vec2 for rect) */
116
- this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
117
- /** @property {number} - How long to stay alive (0 is forever) */
118
- this.emitTime = emitTime;
119
- /** @property {number} - How many particles per second to spawn, does not emit if 0 */
120
- this.emitRate = emitRate;
121
- /** @property {number} - Local angle to apply velocity to particles from emitter */
122
- this.emitConeAngle = emitConeAngle;
123
-
124
- // color settings
125
- /** @property {Color} - Color at start of life 1, randomized between start colors */
126
- this.colorStartA = colorStartA.copy();
127
- /** @property {Color} - Color at start of life 2, randomized between start colors */
128
- this.colorStartB = colorStartB.copy();
129
- /** @property {Color} - Color at end of life 1, randomized between end colors */
130
- this.colorEndA = colorEndA.copy();
131
- /** @property {Color} - Color at end of life 2, randomized between end colors */
132
- this.colorEndB = colorEndB.copy();
133
- /** @property {boolean} - Should color be randomized linearly or across each component */
134
- this.randomColorLinear = randomColorLinear;
135
-
136
- // particle settings
137
- /** @property {number} - How long particles live */
138
- this.particleTime = particleTime;
139
- /** @property {number} - How big are particles at start */
140
- this.sizeStart = sizeStart;
141
- /** @property {number} - How big are particles at end */
142
- this.sizeEnd = sizeEnd;
143
- /** @property {number} - Particle speed when spawned, in world units per frame (at 60fps) */
144
- this.speed = speed;
145
- /** @property {number} - Particle angular speed when spawned, in radians per frame (at 60fps) */
146
- this.angleSpeed = angleSpeed;
147
- /** @property {number} - Per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame) */
148
- this.damping = damping;
149
- /** @property {number} - Per-frame angular velocity multiplier (1 = no damping) */
150
- this.angleDamping = angleDamping;
151
- /** @property {number} - How much gravity affects particles */
152
- this.gravityScale = gravityScale;
153
- /** @property {number} - Cone for start particle angle */
154
- this.particleConeAngle = particleConeAngle;
155
- /** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
156
- this.fadeRate = fadeRate;
157
- /** @property {number} - Apply extra randomness percent */
158
- this.randomness = randomness;
159
- /** @property {boolean} - Do particles collide against tiles */
160
- this.collideTiles = collideTiles;
161
- /** @property {boolean} - Should particles use additive blend */
162
- this.additive = additive;
163
- /** @property {boolean} - Should it be in local space of emitter */
164
- this.localSpace = localSpace;
165
- /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
166
- this.trailScale = 0;
167
- /** @property {ParticleCallback} - Callback when particle is created */
168
- this.particleCreateCallback = undefined;
169
- /** @property {ParticleCallback} - Callback when particle is destroyed */
170
- this.particleDestroyCallback = undefined;
171
- /** @property {ParticleCollideCallback} - Callback when particle collides */
172
- this.particleCollideCallback = undefined;
173
- /** @property {number} - Percentage of velocity to pass to particles (0-1) */
174
- this.velocityInheritance = 0;
175
- /** @property {number} - Track particle emit time */
176
- this.emitTimeBuffer = 0;
177
- /** @property {Array<Particle>} - Array of particles for this emitter */
178
- this.particles = [];
179
-
180
- // track previous position and angle
181
- this.previousAngle = this.angle;
182
- this.previousPos = this.pos.copy();
183
- }
184
-
185
- /** Update the emitter to spawn particles, called automatically by engine once each frame */
186
- update()
187
- {
188
- // physics sanity checks
189
- ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
190
- ASSERT(this.damping >= 0 && this.damping <= 1);
191
-
192
- if (this.velocityInheritance)
193
- {
194
- // pass emitter velocity to particles
195
- const p = this.velocityInheritance;
196
- this.velocity.x = p * (this.pos.x - this.previousPos.x);
197
- this.velocity.y = p * (this.pos.y - this.previousPos.y);
198
- this.angleVelocity = p * (this.angle - this.previousAngle);
199
- this.previousAngle = this.angle;
200
- this.previousPos.x = this.pos.x;
201
- this.previousPos.y = this.pos.y;
202
- }
203
-
204
- // update emitter
205
- if (this.isActive())
206
- {
207
- // emit particles
208
- if (this.emitRate && particleEmitRateScale)
209
- {
210
- const rate = 1/this.emitRate/particleEmitRateScale;
211
- for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
212
- this.emitParticle();
213
- }
214
- }
215
- else if (this.particles.length === 0)
216
- this.destroy(true);
217
-
218
- // update and remove destroyed particles in place to avoid per-frame array allocation
219
- const particles = this.particles;
220
- let alive = 0;
221
- for (let i = 0; i < particles.length; ++i)
222
- {
223
- const p = particles[i];
224
- p.update();
225
- if (!p.destroyed) particles[alive++] = p;
226
- }
227
- particles.length = alive;
228
-
229
- if (debugParticles)
230
- {
231
- // show emitter bounds
232
- if (this.emitCircle)
233
- debugCircle(this.pos, this.emitSize.x/2, '#0f0');
234
- else
235
- debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle);
236
- }
237
- }
238
-
239
- /** Spawn one particle
240
- * @return {Particle} */
241
- emitParticle()
242
- {
243
- // spawn a particle
244
- let pos = this.emitCircle ? // check if circle emitter
245
- randInCircle(this.emitSize.x/2) // circle emitter
246
- : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
247
- .multiply(this.emitSize).rotate(this.angle)
248
- let angle = rand(this.particleConeAngle, -this.particleConeAngle);
249
- if (!this.localSpace)
250
- {
251
- pos.x += this.pos.x;
252
- pos.y += this.pos.y;
253
- angle += this.angle;
254
- }
255
-
256
- // randomness scales each parameter by a percentage
257
- const randomness = this.randomness;
258
- const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
259
-
260
- // randomize particle settings
261
- const particleTime = randomizeScale(this.particleTime);
262
- const sizeStart = randomizeScale(this.sizeStart);
263
- const sizeEnd = randomizeScale(this.sizeEnd);
264
- const speed = randomizeScale(this.speed);
265
- const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
266
- const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
267
- const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
268
- const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
269
- const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
270
-
271
- // build particle
272
- const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
273
- let angleVelocity = angleSpeed;
274
- if (!this.localSpace && this.velocityInheritance > 0)
275
- {
276
- // apply emitter velocity to particle
277
- velocity.x += this.velocity.x;
278
- velocity.y += this.velocity.y;
279
- angleVelocity += this.angleVelocity;
280
- }
281
- const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
282
- this.particles.push(particle);
283
-
284
- // call particle create callback
285
- this.particleCreateCallback?.(particle);
286
-
287
- // return the newly created particle
288
- return particle;
289
- }
290
-
291
- /** Particle emitters do not have physics */
292
- updatePhysics() {}
293
-
294
- /** Render all particles for this emitter */
295
- render()
296
- {
297
- // render all particles
298
- for (const particle of this.particles)
299
- particle.render();
300
- }
301
-
302
- /** is emitter actively spawning */
303
- isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
304
-
305
- /** Destroy the particle emitter
306
- * @param {boolean} [immediate] - should particle emitters and other attached effects be allowed to die off */
307
- destroy(immediate=false)
308
- {
309
- if (this.destroyed) return;
310
-
311
- super.destroy(immediate);
312
- if (!immediate && this.particles.length > 0)
313
- {
314
- // wait for particles to die off
315
- this.destroyed = false;
316
- this.emitTime = -1;
317
- }
318
- }
319
- }
320
-
321
- ///////////////////////////////////////////////////////////////////////////////
322
- // scratch vector reused by Particle.render to avoid per-frame allocations
323
- const particleDrawPos = new Vector2;
324
-
325
- /**
326
- * Particle Object - Created automatically by Particle Emitters
327
- * @memberof Particles
328
- */
329
- class Particle
330
- {
331
- /**
332
- * Create a particle with the passed in settings
333
- * Typically this is created automatically by a ParticleEmitter
334
- * @param {ParticleEmitter} emitter - The emitter that created this particle
335
- * @param {Vector2} pos - World or local space position
336
- * @param {number} angle - Angle of the particle
337
- * @param {Color} colorStart - Color at start of life
338
- * @param {Color} colorEnd - Color at end of life
339
- * @param {number} lifeTime - How long to live for
340
- * @param {number} sizeStart - Size at start of life
341
- * @param {number} sizeEnd - Size at end of life
342
- * @param {Vector2} [velocity] - Velocity of the particle
343
- * @param {number} [angleVelocity] - Angular speed of the particle
344
- */
345
- constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
346
- {
347
- /** @property {ParticleEmitter} */
348
- this.emitter = emitter;
349
- /** @property {Vector2} */
350
- this.pos = pos;
351
- /** @property {number} */
352
- this.angle = angle;
353
- /** @property {Vector2} */
354
- this.size = vec2(sizeStart);
355
- /** @property {Color} */
356
- this.color = colorStart.copy();
357
- /** @property {Color} */
358
- this.colorStart = colorStart;
359
- /** @property {Color} */
360
- this.colorEnd = colorEnd;
361
- /** @property {number} */
362
- this.lifeTime = lifeTime;
363
- /** @property {number} */
364
- this.sizeStart = sizeStart;
365
- /** @property {number} */
366
- this.sizeEnd = sizeEnd;
367
- /** @property {Vector2} */
368
- this.velocity = velocity;
369
- /** @property {number} */
370
- this.angleVelocity = angleVelocity;
371
- /** @property {number} */
372
- this.spawnTime = time;
373
- /** @property {boolean} */
374
- this.mirror = randBool();
375
- /** @property {EngineObject} */
376
- this.groundObject = undefined;
377
- /** @property {boolean} */
378
- this.destroyed = false;
379
- /** @property {TileInfo} */
380
- this.tileInfo = emitter.tileInfo;
381
- }
382
-
383
- /** Update the particle */
384
- update()
385
- {
386
- // emitter properties
387
- const emitter = this.emitter;
388
- const damping = emitter.damping;
389
- const angleDamping = emitter.angleDamping;
390
- const restitution = emitter.restitution;
391
- const friction = emitter.friction;
392
- const gravityScale = emitter.gravityScale;
393
- const collideTiles = emitter.collideTiles;
394
- const collideCallback = emitter.particleCollideCallback;
395
-
396
- // destroy particle when its time runs out
397
- if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
398
- {
399
- this.destroy();
400
- return;
401
- }
402
-
403
- // apply physics
404
- const oldPos = this.pos.copy();
405
- this.velocity.x *= damping;
406
- this.velocity.y *= damping;
407
- this.pos.x += this.velocity.x += gravity.x * gravityScale;
408
- this.pos.y += this.velocity.y += gravity.y * gravityScale;
409
- this.angle += this.angleVelocity *= angleDamping;
410
-
411
- // don't do collision if solver disabled
412
- if (!enablePhysicsSolver || !collideTiles) return;
413
-
414
- // apply max circular speed to prevent going through collision
415
- const length2 = this.velocity.lengthSquared();
416
- if (length2 > objectMaxSpeed*objectMaxSpeed)
417
- {
418
- const s = objectMaxSpeed / length2**.5;
419
- this.velocity.x *= s;
420
- this.velocity.y *= s;
421
- }
422
-
423
- // check collision against tiles
424
- this.groundObject = undefined;
425
- const testCollision = collideCallback ? (pos)=>
426
- {
427
- const data = tileCollisionGetData(pos);
428
- return data && collideCallback(this, data, pos);
429
- } : (pos)=> tileCollisionGetData(pos) > 0;
430
-
431
- if (testCollision(this.pos))
432
- {
433
- // if already was stuck in collision, don't do anything
434
- const hitLayer = tileCollisionTest(this.pos);
435
- if (!testCollision(oldPos))
436
- {
437
- // test which side we bounced off (or both if a corner)
438
- const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
439
- const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
440
- // collide callback may hit where the layer test does not, so hitLayer can be undefined
441
- const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
442
- const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
443
- if (isBlockedX)
444
- {
445
- // move to previous X position and bounce
446
- this.pos.x = oldPos.x;
447
- this.velocity.x *= -hitRestitution;
448
- this.velocity.y *= hitFriction;
449
- }
450
- if (isBlockedY || !isBlockedX)
451
- {
452
- const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
453
- if (wasFalling)
454
- this.groundObject = hitLayer;
455
-
456
- // move to previous Y position and bounce
457
- this.pos.y = oldPos.y;
458
- this.velocity.y *= -hitRestitution;
459
- this.velocity.x *= hitFriction;
460
- }
461
- debugPhysics && debugRect(this.pos, this.size, '#f00');
462
- }
463
- }
464
- }
465
-
466
- /** Destroy this particle */
467
- destroy()
468
- {
469
- const destroyCallback = this.emitter.particleDestroyCallback;
470
- const c = this.colorEnd;
471
- this.color.set(c.r, c.g, c.b, c.a);
472
- this.size.set(this.sizeEnd, this.sizeEnd);
473
- this.destroyed = true;
474
- destroyCallback?.(this);
475
- }
476
-
477
- /** Render the particle, automatically called each frame */
478
- render()
479
- {
480
- // emitter properties
481
- const emitter = this.emitter;
482
- const localSpace = emitter.localSpace;
483
- const additive = emitter.additive;
484
- const trailScale = emitter.trailScale;
485
- const fadeRate = emitter.fadeRate / 2;
486
-
487
- // lerp color and size
488
- const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
489
- const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
490
- const size = vec2(radius);
491
- const alphaFade = p1 < fadeRate ? p1/fadeRate :
492
- p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
493
- this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
494
- this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
495
- this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
496
- this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
497
-
498
- // update the position and angle for drawing
499
- const pos = particleDrawPos.set(this.pos.x, this.pos.y);
500
- let angle = this.angle;
501
- if (localSpace)
502
- {
503
- // in local space of emitter
504
- const a = emitter.angle;
505
- const c = cos(-a), s = sin(-a);
506
- pos.set(emitter.pos.x + pos.x*c - pos.y*s,
507
- emitter.pos.y + pos.x*s + pos.y*c);
508
- angle += a;
509
- }
510
-
511
- // draw the particle
512
- additive && setAdditiveBlendMode();
513
- if (trailScale)
514
- {
515
- // trail style particles
516
- const velocity = localSpace ?
517
- this.velocity.rotate(emitter.angle) : this.velocity;
518
- const speed = velocity.length();
519
- if (speed)
520
- {
521
- // stretch in direction of motion
522
- const trailLength = speed * trailScale;
523
- size.y = max(size.x, trailLength);
524
- angle = atan2(velocity.x, velocity.y);
525
- drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
526
- }
527
- }
528
- else
529
- drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
530
- additive && setAdditiveBlendMode(false);
531
- debugParticles && debugRect(pos, size, '#f005', 0, angle);
532
- }
1
+ /**
2
+ * LittleJS Particle System
3
+ * - Fast and flexible particle effects system
4
+ * - ParticleEmitter spawns and manages lightweight Particle objects
5
+ * - Particles support color gradients, fading, rotation, and scaling
6
+ * - Physics simulation with velocity, gravity, and damping
7
+ * - Collision detection with tile layers
8
+ * - Additive blending for glowing effects
9
+ * - Cone-based emission with randomization
10
+ * - Particle design tool available for easy emitter creation
11
+ * @namespace Particles
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * @callback ParticleCallback - Function that processes a particle
18
+ * @param {Particle} particle
19
+ * @memberof Particles
20
+ */
21
+
22
+ /**
23
+ * @callback ParticleCollideCallback - Decides whether a particle stops at a tile, it is a filter rather than a notice
24
+ * @param {Particle} particle
25
+ * @param {number} tileData
26
+ * @param {Vector2} pos
27
+ * @return {boolean} - true to stop the particle there; a callback that returns nothing lets it pass through
28
+ * @memberof Particles
29
+ */
30
+
31
+ /**
32
+ * Particle Emitter - Spawns particles with the given settings
33
+ * @extends EngineObject
34
+ * @memberof Particles
35
+ * @example
36
+ * // create a particle emitter
37
+ * let pos = vec2(2,3);
38
+ * let particleEmitter = new ParticleEmitter
39
+ * (
40
+ * pos, 0, 1, 0, 500, PI, // pos, angle, emitSize, emitTime, emitRate, emitCone
41
+ * tile(0, 16), // tileInfo
42
+ * rgb(1,1,1,1), rgb(0,0,0,1), // colorStartA, colorStartB
43
+ * rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
44
+ * 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
45
+ * .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
46
+ * .5, 1 // randomness, collide
47
+ * );
48
+ */
49
+ class ParticleEmitter extends EngineObject
50
+ {
51
+ /** Create a particle system with the given settings
52
+ * @param {Vector2} pos - World space position of the emitter
53
+ * @param {number} [angle] - Angle to emit the particles
54
+ * @param {number|Vector2} [emitSize] - World space size of the emitter (float for circle diameter, vec2 for rect)
55
+ * @param {number} [emitTime] - How long to stay alive (0 is forever)
56
+ * @param {number} [emitRate] - How many particles per second to spawn, does not emit if 0
57
+ * @param {number} [emitConeAngle=PI] - Half angle of the cone around the emitter's angle that particles move along, PI is every direction
58
+ * @param {TileInfo} [tileInfo] - Tile info to render particles (undefined is untextured)
59
+ * @param {Color} [colorStartA=WHITE] - Color at start of life 1, randomized between start colors
60
+ * @param {Color} [colorStartB=WHITE] - Color at start of life 2, randomized between start colors
61
+ * @param {Color} [colorEndA=CLEAR_WHITE] - Color at end of life 1, randomized between end colors
62
+ * @param {Color} [colorEndB=CLEAR_WHITE] - Color at end of life 2, randomized between end colors
63
+ * @param {number} [particleTime] - How long particles live
64
+ * @param {number} [sizeStart] - How big are particles at start
65
+ * @param {number} [sizeEnd] - How big are particles at end
66
+ * @param {number} [speed] - How fast are particles when spawned, in world units per frame (at 60fps, so multiply units/sec by 1/60)
67
+ * @param {number} [angleSpeed] - How fast are particles rotating, in radians per frame (at 60fps)
68
+ * @param {number} [damping] - How much to dampen particle speed, per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame)
69
+ * @param {number} [angleDamping] - How much to dampen particle angular speed, per-frame multiplier (1 = no damping)
70
+ * @param {number} [gravityScale] - How much gravity effect particles
71
+ * @param {number} [particleConeAngle] - Half angle each side of the emitter's angle for a particle's start angle, PI is any angle
72
+ * @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
73
+ * @param {number} [randomness] - Apply extra randomness percent
74
+ * @param {boolean} [collideLevel] - Do particles collide with the level's tiles, world space emitters only
75
+ * @param {boolean} [additive] - Should particles use additive blend
76
+ * @param {boolean} [randomColorLinear] - Should color be randomized linearly or across each component
77
+ * @param {number} [renderOrder] - Render order for particles (additive is above other stuff by default)
78
+ * @param {boolean} [localSpace] - Should it be in local space of emitter (world space is default)
79
+ */
80
+ constructor
81
+ (
82
+ pos,
83
+ angle,
84
+ emitSize = 0,
85
+ emitTime = 0,
86
+ emitRate = 100,
87
+ emitConeAngle = PI,
88
+ tileInfo,
89
+ colorStartA = WHITE,
90
+ colorStartB = WHITE,
91
+ colorEndA = CLEAR_WHITE,
92
+ colorEndB = CLEAR_WHITE,
93
+ particleTime = .5,
94
+ sizeStart = .1,
95
+ sizeEnd = 1,
96
+ speed = .1,
97
+ angleSpeed = .05,
98
+ damping = 1,
99
+ angleDamping = 1,
100
+ gravityScale = 0,
101
+ particleConeAngle = PI,
102
+ fadeRate = .1,
103
+ randomness = .2,
104
+ collideLevel = false,
105
+ additive = false,
106
+ randomColorLinear = true,
107
+ renderOrder = additive ? 1e9 : 0,
108
+ localSpace = false
109
+ )
110
+ {
111
+ super(pos, vec2(), tileInfo, angle, undefined, renderOrder);
112
+
113
+ // emitter settings
114
+ /** @property {boolean} - Should particles be emitted in a circle */
115
+ this.emitCircle = typeof emitSize === 'number';
116
+ /** @property {Vector2} - World space size of the emitter, x is the diameter when emitCircle is set */
117
+ this.emitSize = typeof emitSize === 'number' ? vec2(emitSize) : emitSize.copy();
118
+ /** @property {number} - How long to stay alive (0 is forever) */
119
+ this.emitTime = emitTime;
120
+ /** @property {number} - How many particles per second to spawn, does not emit if 0 */
121
+ this.emitRate = emitRate;
122
+ /** @property {number} - Half angle of the cone around the emitter's angle that particles move along, PI is every direction */
123
+ this.emitConeAngle = emitConeAngle;
124
+
125
+ // color settings
126
+ /** @property {Color} - Color at start of life 1, randomized between start colors */
127
+ this.colorStartA = colorStartA.copy();
128
+ /** @property {Color} - Color at start of life 2, randomized between start colors */
129
+ this.colorStartB = colorStartB.copy();
130
+ /** @property {Color} - Color at end of life 1, randomized between end colors */
131
+ this.colorEndA = colorEndA.copy();
132
+ /** @property {Color} - Color at end of life 2, randomized between end colors */
133
+ this.colorEndB = colorEndB.copy();
134
+ /** @property {boolean} - Should color be randomized linearly or across each component */
135
+ this.randomColorLinear = randomColorLinear;
136
+
137
+ // particle settings
138
+ /** @property {number} - How long particles live */
139
+ this.particleTime = particleTime;
140
+ /** @property {number} - How big are particles at start */
141
+ this.sizeStart = sizeStart;
142
+ /** @property {number} - How big are particles at end */
143
+ this.sizeEnd = sizeEnd;
144
+ /** @property {number} - Particle speed when spawned, in world units per frame (at 60fps) */
145
+ this.speed = speed;
146
+ /** @property {number} - Particle angular speed when spawned, in radians per frame (at 60fps) */
147
+ this.angleSpeed = angleSpeed;
148
+ /** @property {number} - Per-frame velocity multiplier (1 = no damping, .9 = lose 10% speed each frame) */
149
+ this.damping = damping;
150
+ /** @property {number} - Per-frame angular velocity multiplier (1 = no damping) */
151
+ this.angleDamping = angleDamping;
152
+ /** @property {number} - How much gravity affects particles */
153
+ this.gravityScale = gravityScale;
154
+ /** @property {number} - Half angle each side of the emitter's angle for a particle's start angle, PI is any angle */
155
+ this.particleConeAngle = particleConeAngle;
156
+ /** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
157
+ this.fadeRate = fadeRate;
158
+ /** @property {number} - Apply extra randomness percent */
159
+ this.randomness = randomness;
160
+ /** @property {boolean} - Do particles collide with the level's tiles */
161
+ this.collideLevel = collideLevel;
162
+ /** @property {boolean} - Should particles use additive blend */
163
+ this.additive = additive;
164
+ /** @property {boolean} - Should it be in local space of emitter */
165
+ this.localSpace = localSpace;
166
+ /** @property {number} - If non zero the particle is drawn as a trail, stretched in the direction of velocity */
167
+ this.trailScale = 0;
168
+ /** @property {ParticleCallback|undefined} - Callback when particle is created
169
+ * @type {ParticleCallback|undefined} */
170
+ this.particleCreateCallback = undefined;
171
+ /** @property {ParticleCallback|undefined} - Callback when particle is destroyed
172
+ * @type {ParticleCallback|undefined} */
173
+ this.particleDestroyCallback = undefined;
174
+ /** @property {ParticleCollideCallback|undefined} - Callback when particle collides
175
+ * @type {ParticleCollideCallback|undefined} */
176
+ this.particleCollideCallback = undefined;
177
+ /** @property {ParticleCallback|undefined} - Callback each time a particle updates, after it moves
178
+ * @type {ParticleCallback|undefined} */
179
+ this.particleUpdateCallback = undefined;
180
+ /** @property {number} - Percentage of velocity to pass to particles (0-1) */
181
+ this.velocityInheritance = 0;
182
+ /** @property {number} - Particles owed to the emit rate, starts at one so the first comes out at once */
183
+ this.emitTimeBuffer = 1;
184
+ /** @property {Array<Particle>} - Array of particles for this emitter
185
+ * @type {Array<Particle>} */
186
+ this.particles = [];
187
+
188
+ // track previous position and angle, set on the first update once a parent has placed the emitter
189
+ /** @type {Vector2|undefined} */
190
+ this.previousPos = undefined;
191
+ this.previousAngle = this.angle;
192
+ }
193
+
194
+ /** Update the emitter to spawn particles, called automatically by engine once each frame */
195
+ update()
196
+ {
197
+ // physics sanity checks
198
+ ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1, 'angleDamping must be 0 to 1, the fraction kept each frame');
199
+ ASSERT(this.damping >= 0 && this.damping <= 1, 'damping must be 0 to 1, the fraction of velocity kept each frame');
200
+
201
+ if (!this.previousPos)
202
+ {
203
+ // first update, addChild moves an emitter after it is made
204
+ this.previousPos = this.pos.copy();
205
+ this.previousAngle = this.angle;
206
+ }
207
+ if (this.velocityInheritance)
208
+ {
209
+ // pass emitter velocity to particles
210
+ const p = this.velocityInheritance;
211
+ this.velocity.x = p * (this.pos.x - this.previousPos.x);
212
+ this.velocity.y = p * (this.pos.y - this.previousPos.y);
213
+ this.angleVelocity = p * (this.angle - this.previousAngle);
214
+ }
215
+ // tracked even while velocityInheritance is off, so turning it on does not jump
216
+ this.previousAngle = this.angle;
217
+ this.previousPos.x = this.pos.x;
218
+ this.previousPos.y = this.pos.y;
219
+
220
+ // update emitter
221
+ if (this.isActive())
222
+ {
223
+ // emit particles
224
+ const rate = this.emitRate * particleEmitRateScale;
225
+ if (rate > 0 && rate < Infinity)
226
+ {
227
+ // counted in particles, so a new rate applies at once
228
+ this.emitTimeBuffer += rate * timeDelta;
229
+ for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
230
+ this.emitParticle();
231
+ }
232
+ }
233
+ else if (this.particles.length === 0)
234
+ this.destroy(true);
235
+
236
+ // a local space particle is placed relative to the emitter, but the tile collision is in the world
237
+ ASSERT(!this.localSpace || !this.collideLevel, 'local space particles cannot collide with the level, turn one of them off');
238
+
239
+ // update and remove destroyed particles in place to avoid per-frame array allocation
240
+ const particles = this.particles;
241
+ let alive = 0;
242
+ for (let i = 0; i < particles.length; ++i)
243
+ {
244
+ const p = particles[i];
245
+ p.update();
246
+ if (!p.destroyed) particles[alive++] = p;
247
+ }
248
+ particles.length = alive;
249
+
250
+ if (debugParticles)
251
+ {
252
+ // show emitter bounds
253
+ if (this.emitCircle)
254
+ debugCircle(this.pos, this.emitSize.x, '#0f0', 0, false, false);
255
+ else
256
+ debugRect(this.pos, this.emitSize, '#0f0', 0, this.angle, false, false);
257
+ }
258
+ }
259
+
260
+ /** Spawn one particle
261
+ * @return {Particle} */
262
+ emitParticle()
263
+ {
264
+ // spawn a particle
265
+ let pos = this.emitCircle ? // check if circle emitter
266
+ randInCircle(this.emitSize.x/2) // circle emitter
267
+ : vec2(rand(-.5,.5), rand(-.5,.5)) // box emitter
268
+ .multiply(this.emitSize);
269
+ let angle = rand(this.particleConeAngle, -this.particleConeAngle);
270
+ if (!this.localSpace)
271
+ {
272
+ // into the world: a local space particle is turned with the emitter when it draws instead
273
+ this.emitCircle || (pos = pos.rotate(this.angle));
274
+ pos.x += this.pos.x;
275
+ pos.y += this.pos.y;
276
+ angle += this.angle;
277
+ }
278
+
279
+ // randomness scales each parameter by a percentage
280
+ const randomness = this.randomness;
281
+ const randomizeScale = (v)=> v + v*rand(randomness, -randomness);
282
+
283
+ // randomize particle settings
284
+ const particleTime = randomizeScale(this.particleTime);
285
+ const sizeStart = randomizeScale(this.sizeStart);
286
+ const sizeEnd = randomizeScale(this.sizeEnd);
287
+ const speed = randomizeScale(this.speed);
288
+ const angleSpeed = randomizeScale(this.angleSpeed) * randSign();
289
+ const coneAngle = rand(this.emitConeAngle, -this.emitConeAngle);
290
+ const colorStart = randColor(this.colorStartA, this.colorStartB, this.randomColorLinear);
291
+ const colorEnd = randColor(this.colorEndA, this.colorEndB, this.randomColorLinear);
292
+ const velocityAngle = this.localSpace ? coneAngle : this.angle + coneAngle;
293
+
294
+ // build particle
295
+ const velocity = vec2(speed*sin(velocityAngle), speed*cos(velocityAngle));
296
+ let angleVelocity = angleSpeed;
297
+ if (!this.localSpace && this.velocityInheritance > 0)
298
+ {
299
+ // apply emitter velocity to particle
300
+ velocity.x += this.velocity.x;
301
+ velocity.y += this.velocity.y;
302
+ angleVelocity += this.angleVelocity;
303
+ }
304
+ const particle = new Particle(this, pos, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, velocity, angleVelocity);
305
+ this.particles.push(particle);
306
+
307
+ // call particle create callback
308
+ this.particleCreateCallback?.(particle);
309
+
310
+ // return the newly created particle
311
+ return particle;
312
+ }
313
+
314
+ /** Particle emitters do not have physics */
315
+ updatePhysics() {}
316
+
317
+ /** Render all particles for this emitter */
318
+ render()
319
+ {
320
+ // render all particles, the blend switched once for them all, not around each one
321
+ this.additive && setAdditiveBlendMode();
322
+ for (const particle of this.particles)
323
+ particle.render();
324
+ this.additive && setAdditiveBlendMode(false);
325
+ }
326
+
327
+ /** is emitter actively spawning */
328
+ isActive() { return !this.emitTime || this.getAliveTime() < this.emitTime; }
329
+
330
+ /** Destroy the particle emitter
331
+ * @param {boolean} [immediate] - true removes attached effects like particle emitters at once, false lets them finish first */
332
+ destroy(immediate=false)
333
+ {
334
+ if (this.destroyed) return;
335
+
336
+ super.destroy(immediate);
337
+ if (!immediate && this.particles.length > 0)
338
+ {
339
+ // wait for particles to die off
340
+ this.destroyed = false;
341
+ this.emitTime = -1;
342
+ }
343
+ }
344
+ }
345
+
346
+ ///////////////////////////////////////////////////////////////////////////////
347
+ // scratch vectors reused by Particle.render and the tile collision to avoid per-frame allocations
348
+ const particleDrawPos = new Vector2, particleDrawSize = new Vector2, particleCollidePos = new Vector2;
349
+
350
+ // tests if a particle collides with tiles at x, y, through its emitter's collide callback if it has one
351
+ function particleCollideTest(particle, collideCallback, x, y)
352
+ {
353
+ const data = tileCollisionGetData(particleCollidePos.set(x, y));
354
+ if (!collideCallback)
355
+ return data > 0;
356
+
357
+ // the callback gets its own vector, made only when there is a tile to decide on
358
+ return !!data && !!collideCallback(particle, data, vec2(x, y));
359
+ }
360
+
361
+ /**
362
+ * Particle Object - Created automatically by Particle Emitters
363
+ * @memberof Particles
364
+ */
365
+ class Particle
366
+ {
367
+ /**
368
+ * Create a particle with the passed in settings
369
+ * Typically this is created automatically by a ParticleEmitter
370
+ * @param {ParticleEmitter} emitter - The emitter that created this particle
371
+ * @param {Vector2} pos - World or local space position
372
+ * @param {number} angle - Angle of the particle
373
+ * @param {Color} colorStart - Color at start of life
374
+ * @param {Color} colorEnd - Color at end of life
375
+ * @param {number} lifeTime - How long to live for
376
+ * @param {number} sizeStart - Size at start of life
377
+ * @param {number} sizeEnd - Size at end of life
378
+ * @param {Vector2} [velocity] - Velocity of the particle
379
+ * @param {number} [angleVelocity] - Angular speed of the particle
380
+ */
381
+ constructor(emitter, pos, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, velocity = vec2(), angleVelocity = 0)
382
+ {
383
+ /** @property {ParticleEmitter} - The emitter this particle came from */
384
+ this.emitter = emitter;
385
+ /** @property {Vector2} - Position, world space or local to the emitter when localSpace is set */
386
+ this.pos = pos;
387
+ /** @property {number} - Angle in radians */
388
+ this.angle = angle;
389
+ /** @property {Vector2} - Current size, updated as it renders */
390
+ this.size = vec2(sizeStart);
391
+ /** @property {Color} - Current color, updated as it renders */
392
+ this.color = colorStart.copy();
393
+ /** @property {Color} - Color at start of life */
394
+ this.colorStart = colorStart;
395
+ /** @property {Color} - Color at end of life */
396
+ this.colorEnd = colorEnd;
397
+ /** @property {number} - How long this particle lives for */
398
+ this.lifeTime = lifeTime;
399
+ /** @property {number} - Size at start of life */
400
+ this.sizeStart = sizeStart;
401
+ /** @property {number} - Size at end of life */
402
+ this.sizeEnd = sizeEnd;
403
+ /** @property {Vector2} - Velocity in world units per frame (at 60fps) */
404
+ this.velocity = velocity;
405
+ /** @property {number} - Angular speed in radians per frame (at 60fps) */
406
+ this.angleVelocity = angleVelocity;
407
+ /** @property {number} - Engine time it was made at */
408
+ this.spawnTime = time;
409
+ /** @property {boolean} - If true the tile is flipped along the x axis */
410
+ this.mirror = randBool();
411
+ /** @property {EngineObject|undefined} - Tile layer it last landed on, undefined in the air
412
+ * @type {EngineObject|undefined} */
413
+ this.groundObject = undefined;
414
+ /** @property {boolean} - Has this particle been destroyed */
415
+ this.destroyed = false;
416
+ /** @property {TileInfo|undefined} - The emitter's tile, undefined for an untextured one
417
+ * @type {TileInfo|undefined} */
418
+ this.tileInfo = emitter.tileInfo;
419
+ }
420
+
421
+ /** Update the particle */
422
+ update()
423
+ {
424
+ if (this.destroyed) return; // gone already, destroyed by the game this frame
425
+
426
+ // emitter properties
427
+ const emitter = this.emitter;
428
+ const damping = emitter.damping;
429
+ const angleDamping = emitter.angleDamping;
430
+ const restitution = emitter.restitution;
431
+ const friction = emitter.friction;
432
+ const gravityScale = emitter.gravityScale;
433
+ const collideLevel = emitter.collideLevel;
434
+ const collideCallback = emitter.particleCollideCallback;
435
+ const updateCallback = emitter.particleUpdateCallback;
436
+
437
+ // destroy particle when its time runs out
438
+ if (this.lifeTime <= 0 || time - this.spawnTime > this.lifeTime) // no lifetime is gone at once, not never
439
+ {
440
+ this.destroy();
441
+ return;
442
+ }
443
+
444
+ // apply physics; only the tile collision needs where the particle was
445
+ const solve = enablePhysicsSolver && collideLevel;
446
+ const oldX = this.pos.x, oldY = this.pos.y;
447
+ let gravityX = gravity.x * gravityScale, gravityY = gravity.y * gravityScale;
448
+ if (emitter.localSpace && emitter.angle)
449
+ {
450
+ // world gravity turned into the emitter's space, the render turns it back
451
+ const c = cos(emitter.angle), s = sin(emitter.angle);
452
+ [gravityX, gravityY] = [gravityX*c - gravityY*s, gravityX*s + gravityY*c];
453
+ }
454
+ this.velocity.x = this.velocity.x * damping + gravityX;
455
+ this.velocity.y = this.velocity.y * damping + gravityY;
456
+ if (solve)
457
+ {
458
+ // apply max circular speed to prevent going through collision, before the move it protects
459
+ const length2 = this.velocity.lengthSquared();
460
+ if (length2 > objectMaxSpeed*objectMaxSpeed)
461
+ {
462
+ const s = objectMaxSpeed / length2**.5;
463
+ this.velocity.x *= s;
464
+ this.velocity.y *= s;
465
+ }
466
+ }
467
+ this.pos.x += this.velocity.x;
468
+ this.pos.y += this.velocity.y;
469
+ this.angle += this.angleVelocity *= angleDamping;
470
+
471
+ // don't do collision if solver disabled
472
+ if (!solve)
473
+ {
474
+ updateCallback?.(this);
475
+ return;
476
+ }
477
+
478
+ // check collision against tiles
479
+ this.groundObject = undefined;
480
+ if (particleCollideTest(this, collideCallback, this.pos.x, this.pos.y))
481
+ {
482
+ // if already was stuck in collision, don't do anything
483
+ const hitLayer = tileCollisionTest(this.pos);
484
+ if (!particleCollideTest(this, collideCallback, oldX, oldY))
485
+ {
486
+ // test which side we bounced off (or both if a corner)
487
+ const isBlockedX = particleCollideTest(this, collideCallback, this.pos.x, oldY);
488
+ const isBlockedY = particleCollideTest(this, collideCallback, oldX, this.pos.y);
489
+ // collide callback may hit where the layer test does not, so hitLayer can be undefined
490
+ const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
491
+ const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
492
+ if (isBlockedX)
493
+ {
494
+ // move to previous X position and bounce
495
+ this.pos.x = oldX;
496
+ this.velocity.x *= -hitRestitution;
497
+ this.velocity.y *= hitFriction;
498
+ }
499
+ if (isBlockedY || !isBlockedX)
500
+ {
501
+ const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
502
+ if (wasFalling)
503
+ this.groundObject = hitLayer;
504
+
505
+ // move to previous Y position and bounce
506
+ this.pos.y = oldY;
507
+ this.velocity.y *= -hitRestitution;
508
+ this.velocity.x *= hitFriction;
509
+ }
510
+ debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
511
+ }
512
+ }
513
+
514
+ // a collide callback may have destroyed it
515
+ this.destroyed || updateCallback?.(this);
516
+ }
517
+
518
+ /** Destroy this particle, once: a second call does nothing
519
+ */
520
+ destroy()
521
+ {
522
+ if (this.destroyed) return;
523
+ const destroyCallback = this.emitter.particleDestroyCallback;
524
+ const c = this.colorEnd;
525
+ this.color.set(c.r, c.g, c.b, c.a);
526
+ this.size.set(this.sizeEnd, this.sizeEnd);
527
+ this.destroyed = true;
528
+ destroyCallback?.(this);
529
+ }
530
+
531
+ /** Render the particle, automatically called each frame */
532
+ render()
533
+ {
534
+ // emitter properties
535
+ const emitter = this.emitter;
536
+ const localSpace = emitter.localSpace;
537
+ const additive = emitter.additive && !glAdditive; // switched here only for a particle drawn on its own
538
+ const trailScale = emitter.trailScale;
539
+ const fadeRate = emitter.fadeRate / 2;
540
+
541
+ // lerp color and size
542
+ const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
543
+ const sizeNow = p2 * this.sizeStart + p1 * this.sizeEnd;
544
+ this.size.set(sizeNow, sizeNow); // kept current for callbacks, drawn from the scratch since a trail stretches it
545
+ const size = particleDrawSize.set(sizeNow, sizeNow);
546
+ const alphaFade = p1 < fadeRate ? p1/fadeRate :
547
+ p1 > 1-fadeRate ? (1-p1)/fadeRate : 1;
548
+ this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
549
+ this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
550
+ this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
551
+ this.color.a = (p2 * this.colorStart.a + p1 * this.colorEnd.a) * alphaFade;
552
+
553
+ // update the position and angle for drawing
554
+ const pos = particleDrawPos.set(this.pos.x, this.pos.y);
555
+ let angle = this.angle;
556
+ if (localSpace)
557
+ {
558
+ // in local space of emitter
559
+ const a = emitter.angle;
560
+ const c = cos(-a), s = sin(-a);
561
+ pos.set(emitter.pos.x + pos.x*c - pos.y*s,
562
+ emitter.pos.y + pos.x*s + pos.y*c);
563
+ angle += a;
564
+ }
565
+
566
+ // draw the particle
567
+ additive && setAdditiveBlendMode();
568
+ if (trailScale)
569
+ {
570
+ // trail style particles stretch in the direction of motion, and draw as they are at rest
571
+ const velocity = localSpace ?
572
+ this.velocity.rotate(emitter.angle) : this.velocity;
573
+ const speed = velocity.length();
574
+ if (speed)
575
+ {
576
+ size.y = max(size.x, speed * trailScale);
577
+ angle = atan2(velocity.x, velocity.y);
578
+ }
579
+ }
580
+ drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror, undefined, glEnable, false);
581
+ additive && setAdditiveBlendMode(false);
582
+ debugParticles && debugRect(pos, size, '#f005', 0, angle, false, false);
583
+ }
533
584
  }