[READ-ONLY] Mirror of https://github.com/flo-bit/pixi-particles. particle system for pixi.js
flo-bit.github.io/pixi-particles/
javascript
particle-system
particles
pixijs
8.5 kB
343 lines
1import * as PIXI from "https://cdn.jsdelivr.net/npm/pixi.js@7.x/dist/pixi.min.mjs";
2
3import Noise from "../every-noise/noise.js";
4import Utils from "../js-utils/utils.js";
5import Vector from "../js-utils/vector.js";
6
7/**
8 * A particle emitter
9 *
10 * @class ParticleEmitter
11 *
12 * @param {object} opts
13 * @param {string} opts.type - "point", "circle", "box", "line"
14 *
15 * @param {number} opts.size - size of circle or box
16 *
17 * @param {number} opts.width - width of box
18 * @param {number} opts.height - height of box
19 *
20 * @param {Vector} opts.center - center of emitter
21 *
22 * @param {Vector} opts.a - start of line
23 * @param {Vector} opts.b - end of line
24 *
25 * @param {number} opts.particlesPerSecond - number of particles to spawn per second
26 *
27 * @param {object} opts.particleSettings - settings to pass to Particle constructor
28 *
29 */
30class ParticleEmitter {
31 constructor(opts) {
32 opts = opts || {};
33 this.type = opts.type ?? "point";
34
35 this.size = new Vector(
36 opts.width ?? opts.w ?? opts.size ?? 10,
37 opts.height ?? opts.h ?? opts.size ?? 10
38 );
39
40 this.center = opts.center ?? new Vector(0, 0);
41 this.a = opts.a;
42 this.b = opts.b;
43 if (this.a && this.b) {
44 this.vec = this.b.clone().sub(this.a);
45 }
46
47 this.particlesPerSecond = opts.particlesPerSecond ?? 100;
48
49 this.particleSettings = opts.particleSettings ?? opts.settings ?? {};
50
51 this.dt = 0;
52 }
53
54 getPos() {
55 if (this.type == "point") {
56 return this.center;
57 } else if (this.type == "circle") {
58 let angle = Math.random() * Math.PI * 2;
59 let r = (Math.random() * this.size.x) / 2;
60 return this.center.clone().add(Math.cos(angle) * r, Math.sin(angle) * r);
61 } else if (
62 this.type == "box" ||
63 this.type == "square" ||
64 this.type == "rect"
65 ) {
66 let x = (Math.random() - 0.5) * this.size.x;
67 let y = (Math.random() - 0.5) * this.size.y;
68 return this.center.clone().add(x, y);
69 } else if (this.type == "line") {
70 let t = Math.random();
71 return this.b.clone().sub(this.a).mult(t, t).add(this.a);
72 }
73 }
74 update(dt) {
75 // spawn new particles
76 this.dt += dt;
77
78 let count = Math.floor(this.particlesPerSecond * this.dt);
79 if (count == 0) return;
80
81 this.dt -= count / this.particlesPerSecond;
82
83 for (let i = 0; i < count; i++) {
84 let pos = this.getPos();
85 this.particleSettings.x = pos.x;
86 this.particleSettings.y = pos.y;
87 this.spawn(this.particleSettings);
88 }
89 }
90}
91
92/**
93 * A particle
94 *
95 * @class Particle
96 *
97 * @param {object} opts
98 *
99 * @param {number|function} opts.x - x position
100 * @param {number|function} opts.y - y position
101 *
102 * @param {number|function} opts.vx - x velocity
103 * @param {number|function} opts.vy - y velocity
104 *
105 * @param {number|function} opts.life - lifetime in seconds
106 *
107 * @param {number|function} opts.size - size of particle relative to max size (1.0 = max size)
108 *
109 * @param {number} opts.color - color of particle
110 * @param {number} opts.alpha - alpha value of particle
111 *
112 * @param {boolean} opts.shouldShrink - should particle shrink over time
113 * @param {boolean} opts.shouldDisappear - should particle disappear over time
114 *
115 * @param {boolean} opts.applyForces - should particle be affected by forces
116 * @param {number} opts.drag - drag coefficient
117 *
118 * @param {function} opts.check - function to check if particle should be removed (return true to remove)
119 */
120class Particle extends PIXI.Sprite {
121 static makeTexture(renderer, size, shape) {
122 size = size ?? 1;
123 const graphic = new PIXI.Graphics();
124 graphic.beginFill(0xffffff);
125 if (shape == "square" || shape == "rect") {
126 graphic.drawRect(-size, -size, size * 2, size * 2);
127 } else {
128 graphic.drawCircle(0, 0, size);
129 }
130 graphic.endFill();
131 let texture = renderer.generateTexture(graphic);
132 return texture;
133 }
134
135 constructor(opts) {
136 opts = opts || {};
137 super(opts.texture);
138
139 this.opts = opts;
140 this._alive = true;
141 this.life = 0;
142 this.shouldShrink = opts.shouldShrink ?? false;
143 this.shouldDisappear = opts.shouldDisappear ?? false;
144
145 this.size = Utils.getNumber(opts.size, this, 1);
146 this.scale.x = this.scale.y = this.size;
147
148 this.lifetimeSeconds = Utils.getNumber(opts.life, this, 0);
149
150 this.x = Utils.getNumber(opts.x, this, 0);
151 this.y = Utils.getNumber(opts.y, this, 0);
152
153 this.vx = Utils.getNumber(opts.vx, this, 0);
154 this.vy = Utils.getNumber(opts.vy, this, 0);
155 this.v = new Vector(this.vx, this.vy);
156
157 this.tint = Utils.getNumber(opts.color ?? opts.tint, this, 0xffffff);
158
159 this.applyForces = opts.applyForces ?? false;
160
161 this.alpha = Utils.getNumber(opts.alpha, this, 1);
162
163 this.check = opts.check;
164
165 this.drag = opts.drag;
166 }
167
168 set color(c) {
169 this.tint = c;
170 }
171 get color() {
172 return this.tint;
173 }
174
175 get alive() {
176 return this._alive;
177 }
178 set alive(value) {
179 this._alive = value;
180 this.alpha = value ? 1 : 0;
181 }
182
183 reset(settings) {
184 for (let k of Object.keys(settings)) {
185 particle[k] = settings[k];
186 }
187 }
188
189 applyForce(force) {
190 if (!this.applyForces) return;
191
192 let value = force(this);
193 if (value == undefined || value == null) return;
194 this.v.add(value);
195 }
196
197 update(dt) {
198 if (!this.alive) return;
199
200 this.life += dt;
201 if (this.lifetimeSeconds > 0 && this.life > this.lifetimeSeconds) {
202 this.alive = false;
203 return;
204 }
205
206 if (this.shouldShrink) {
207 this.scale.x = this.scale.y =
208 this.size * (1 - this.life / this.lifetimeSeconds);
209 }
210 if (this.shouldDisappear) {
211 this.alpha = 1 - this.life / this.lifetimeSeconds;
212 }
213 if (this.drag != undefined) {
214 this.v.x *= this.drag;
215 this.v.y *= this.drag;
216 }
217
218 this.x += this.v.x * dt;
219 this.y += this.v.y * dt;
220
221 if (this.check && this.check(this)) {
222 this.alive = false;
223 }
224 }
225}
226
227/**
228 * A particle container
229 *
230 * @class ParticleContainer
231 *
232 * @param {object} opts
233 * @param {number} opts.maxCount - maximum number of particles
234 * @param {number} opts.maxSize - maximum size of particles
235 * @param {string} opts.shape - shape of particles (circle or square)
236 *
237 * @param {PIXI.Renderer} opts.renderer - renderer to use for generating textures
238 *
239 */
240class ParticleContainer extends PIXI.ParticleContainer {
241 constructor(opts) {
242 opts = opts || {};
243 let maxParticleCount = opts.maxCount ?? 10000;
244 super(
245 maxParticleCount,
246 {
247 vertices: true,
248 position: true,
249 rotation: true,
250 tint: true,
251 },
252 undefined,
253 true
254 );
255
256 this.particleTexture =
257 opts.texture ??
258 Particle.makeTexture(
259 opts.renderer,
260 opts.maxSize ?? opts.size,
261 opts.shape
262 );
263
264 this.emitters = [];
265 if (opts.emitters) {
266 for (let e of opts.emitters) {
267 this.createEmitter(e);
268 }
269 }
270
271 this.forces = [];
272 if (opts.forces) {
273 for (let f of opts.forces) {
274 this.forces.push(f);
275 }
276 }
277 }
278 createEmitter(options) {
279 let emitter = new ParticleEmitter(options);
280 emitter.spawn = this.spawn.bind(this);
281 this.emitters.push(emitter);
282 return emitter;
283 }
284 spawn(opts) {
285 opts = opts || {};
286 let count = Utils.getNumber(opts.count, this);
287 if (count == undefined) count = 1;
288 for (let i = 0; i < count; i++) {
289 this.createParticle(opts);
290 }
291 }
292 createParticle(settings) {
293 settings.texture = this.particleTexture;
294 let particle = new Particle(settings);
295 this.addChild(particle);
296 return particle;
297 }
298
299 /**
300 * function does the following:
301 *
302 * - updates all emitters
303 * - updates all particles
304 * - applies forces to all particles
305 * - removes dead particles
306 *
307 * @param {*} dt
308 */
309 update(dt) {
310 // go through all emitters and add new particles to the queue
311 for (let emitter of this.emitters) {
312 emitter.update(dt);
313 }
314
315 // update all living particles
316 for (let i = this.children.length - 1; i >= 0; i--) {
317 let particle = this.children[i];
318
319 // if particle is dead, remove it
320 if (!particle.alive) {
321 this.children.splice(i, 1);
322 continue;
323 }
324 particle.update(dt);
325
326 // apply forces
327 for (let force of this.forces) {
328 particle.applyForce(force);
329 }
330 }
331 }
332}
333
334export {
335 ParticleContainer as Particles,
336 Particle,
337 ParticleEmitter,
338 Utils,
339 Noise,
340 Vector,
341 PIXI,
342};
343export default ParticleContainer;