[READ-ONLY] Mirror of https://github.com/flo-bit/bluesky-particles.
flo-bit.dev/bluesky-visualizers/particles
3.0 kB
109 lines
1import * as PIXI from "pixi.js";
2
3interface ParticleSprite extends PIXI.Sprite {
4 speedX: number;
5 speedY: number;
6 age: number;
7 maxAge: number;
8 initialSize: number;
9}
10
11export type ParticleOptions = {
12 x: number;
13 y: number;
14 size: number;
15 color: PIXI.ColorSource;
16 alpha: number;
17 speedX: number;
18 speedY: number;
19 maxAge: number;
20};
21
22export default class ParticleSystem {
23 private app: PIXI.Application;
24 private maxParticles: number;
25 public container: PIXI.Container;
26 private particles: ParticleSprite[];
27 private particlePool: ParticleSprite[];
28 private baseTexture: PIXI.Texture;
29
30 private loaded = false;
31
32 constructor(texture: string, maxParticles: number = 10000) {
33 this.maxParticles = maxParticles;
34 this.container = new PIXI.Container();
35 this.container.zIndex = 10;
36 this.particles = [];
37 this.particlePool = [];
38 this.loadTexture(texture);
39 }
40
41 async loadTexture(url: string) {
42 const texture = await PIXI.Assets.load(url);
43 this.baseTexture = texture;
44 this.loaded = true;
45 }
46
47 spawnParticle(opts: Partial<ParticleOptions>): ParticleSprite | null {
48 if (!this.loaded) return null;
49 let particle: ParticleSprite;
50 if (this.particlePool.length > 0) {
51 particle = this.particlePool.pop()!;
52 } else if (this.particles.length < this.maxParticles) {
53 particle = new PIXI.Sprite(this.baseTexture) as ParticleSprite;
54 particle.anchor.set(0.5, 0.5);
55 this.container.addChild(particle);
56 } else {
57 return null;
58 }
59
60 particle.rotation = (Math.random() - 0.5) * 0.5;
61 particle.visible = true;
62 particle.x = opts.x ?? 0;
63 particle.y = opts.y ?? 0;
64 particle.width = particle.height = opts.size ?? 1;
65 particle.tint = opts.color ?? 0xffffff;
66 particle.alpha = opts.alpha ?? 1;
67 particle.speedX = opts.speedX ?? 0;
68 particle.speedY = opts.speedY ?? 0;
69 particle.age = 0;
70 particle.maxAge = opts.maxAge ?? 1;
71 particle.initialSize = opts.size ?? 1;
72
73 this.particles.push(particle);
74
75 return particle;
76 }
77
78 removeParticle(particle: ParticleSprite): void {
79 const index = this.particles.indexOf(particle);
80 if (index !== -1) {
81 particle.visible = false;
82
83 this.particles.splice(index, 1);
84 this.particlePool.push(particle);
85 }
86 }
87
88 update(deltaTime: number): void {
89 for (let i = this.particles.length - 1; i >= 0; i--) {
90 const particle = this.particles[i];
91 particle.x += particle.speedX * deltaTime;
92 particle.y += particle.speedY * deltaTime;
93 particle.age += deltaTime;
94
95 // Shrink the particle
96 let lifeRatio = 1 - particle.age / particle.maxAge;
97 lifeRatio = Math.min(Math.max(lifeRatio, 0), 1);
98 particle.width = particle.height = particle.initialSize * lifeRatio;
99
100 // Remove particle if it's too old or off-screen
101 if (
102 particle.age >= particle.maxAge ||
103 particle.initialSize * lifeRatio < 4
104 ) {
105 this.removeParticle(particle);
106 }
107 }
108 }
109}