@basmilius/sparkle 2.0.0 → 2.2.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 (129) hide show
  1. package/dist/index.d.mts +1053 -28
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +4840 -400
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +7 -2
  6. package/src/aurora/consts.ts +3 -0
  7. package/src/aurora/index.ts +10 -0
  8. package/src/aurora/layer.ts +180 -0
  9. package/src/aurora/types.ts +13 -0
  10. package/src/balloons/consts.ts +3 -0
  11. package/src/balloons/index.ts +12 -0
  12. package/src/balloons/layer.ts +169 -0
  13. package/src/balloons/particle.ts +110 -0
  14. package/src/balloons/types.ts +14 -0
  15. package/src/bubbles/consts.ts +3 -0
  16. package/src/bubbles/index.ts +10 -0
  17. package/src/bubbles/layer.ts +246 -0
  18. package/src/bubbles/types.ts +21 -0
  19. package/src/canvas.ts +32 -1
  20. package/src/color.ts +19 -0
  21. package/src/confetti/consts.ts +13 -13
  22. package/src/confetti/index.ts +20 -2
  23. package/src/confetti/layer.ts +155 -0
  24. package/src/confetti/particle.ts +106 -0
  25. package/src/confetti/shapes.ts +104 -0
  26. package/src/confetti/types.ts +4 -1
  27. package/src/distance.ts +1 -1
  28. package/src/donuts/consts.ts +19 -0
  29. package/src/donuts/donut.ts +12 -0
  30. package/src/donuts/index.ts +9 -0
  31. package/src/donuts/layer.ts +301 -0
  32. package/src/effect.ts +107 -0
  33. package/src/fade.ts +87 -0
  34. package/src/fireflies/consts.ts +3 -0
  35. package/src/fireflies/index.ts +12 -0
  36. package/src/fireflies/layer.ts +169 -0
  37. package/src/fireflies/particle.ts +124 -0
  38. package/src/fireflies/types.ts +17 -0
  39. package/src/firepit/consts.ts +3 -0
  40. package/src/firepit/index.ts +10 -0
  41. package/src/firepit/layer.ts +193 -0
  42. package/src/firepit/types.ts +20 -0
  43. package/src/fireworks/create-explosion.ts +237 -0
  44. package/src/fireworks/explosion.ts +9 -9
  45. package/src/fireworks/firework.ts +9 -8
  46. package/src/fireworks/index.ts +19 -3
  47. package/src/fireworks/layer.ts +203 -0
  48. package/src/fireworks/spark.ts +9 -9
  49. package/src/fireworks/types.ts +2 -2
  50. package/src/glitter/consts.ts +13 -0
  51. package/src/glitter/index.ts +9 -0
  52. package/src/glitter/layer.ts +181 -0
  53. package/src/glitter/types.ts +33 -0
  54. package/src/index.ts +27 -0
  55. package/src/lanterns/consts.ts +13 -0
  56. package/src/lanterns/index.ts +9 -0
  57. package/src/lanterns/layer.ts +178 -0
  58. package/src/lanterns/types.ts +22 -0
  59. package/src/layer.ts +26 -0
  60. package/src/leaves/consts.ts +16 -0
  61. package/src/leaves/index.ts +9 -0
  62. package/src/leaves/layer.ts +258 -0
  63. package/src/leaves/types.ts +25 -0
  64. package/src/lightning/consts.ts +3 -0
  65. package/src/lightning/index.ts +11 -0
  66. package/src/lightning/layer.ts +41 -0
  67. package/src/lightning/system.ts +196 -0
  68. package/src/lightning/types.ts +20 -0
  69. package/src/matrix/consts.ts +5 -0
  70. package/src/matrix/index.ts +9 -0
  71. package/src/matrix/layer.ts +154 -0
  72. package/src/matrix/types.ts +17 -0
  73. package/src/orbits/consts.ts +13 -0
  74. package/src/orbits/index.ts +9 -0
  75. package/src/orbits/layer.ts +213 -0
  76. package/src/orbits/types.ts +27 -0
  77. package/src/particles/consts.ts +3 -0
  78. package/src/particles/index.ts +10 -0
  79. package/src/particles/layer.ts +360 -0
  80. package/src/particles/types.ts +10 -0
  81. package/src/petals/consts.ts +13 -0
  82. package/src/petals/index.ts +10 -0
  83. package/src/petals/layer.ts +174 -0
  84. package/src/petals/types.ts +15 -0
  85. package/src/plasma/consts.ts +3 -0
  86. package/src/plasma/index.ts +10 -0
  87. package/src/plasma/layer.ts +107 -0
  88. package/src/plasma/types.ts +5 -0
  89. package/src/rain/consts.ts +3 -0
  90. package/src/rain/index.ts +12 -0
  91. package/src/rain/layer.ts +194 -0
  92. package/src/rain/particle.ts +132 -0
  93. package/src/rain/types.ts +22 -0
  94. package/src/sandstorm/consts.ts +3 -0
  95. package/src/sandstorm/index.ts +10 -0
  96. package/src/sandstorm/layer.ts +152 -0
  97. package/src/sandstorm/types.ts +10 -0
  98. package/src/scene.ts +201 -0
  99. package/src/shooting-stars/index.ts +3 -0
  100. package/src/shooting-stars/system.ts +151 -0
  101. package/src/shooting-stars/types.ts +11 -0
  102. package/src/simulation-canvas.ts +83 -0
  103. package/src/snow/consts.ts +2 -2
  104. package/src/snow/index.ts +9 -2
  105. package/src/snow/{simulation.ts → layer.ts} +64 -89
  106. package/src/sparklers/consts.ts +3 -0
  107. package/src/sparklers/index.ts +16 -0
  108. package/src/sparklers/layer.ts +220 -0
  109. package/src/sparklers/particle.ts +89 -0
  110. package/src/sparklers/types.ts +13 -0
  111. package/src/stars/consts.ts +3 -0
  112. package/src/stars/index.ts +10 -0
  113. package/src/stars/layer.ts +139 -0
  114. package/src/stars/types.ts +12 -0
  115. package/src/streamers/consts.ts +14 -0
  116. package/src/streamers/index.ts +10 -0
  117. package/src/streamers/layer.ts +223 -0
  118. package/src/streamers/types.ts +14 -0
  119. package/src/trail.ts +140 -0
  120. package/src/waves/consts.ts +3 -0
  121. package/src/waves/index.ts +10 -0
  122. package/src/waves/layer.ts +164 -0
  123. package/src/waves/types.ts +10 -0
  124. package/src/wormhole/consts.ts +3 -0
  125. package/src/wormhole/index.ts +10 -0
  126. package/src/wormhole/layer.ts +197 -0
  127. package/src/wormhole/types.ts +10 -0
  128. package/src/confetti/simulation.ts +0 -221
  129. package/src/fireworks/simulation.ts +0 -493
@@ -0,0 +1,237 @@
1
+ import type { Point } from '../point';
2
+ import { Explosion } from './explosion';
3
+ import { EXPLOSION_CONFIGS, type ExplosionType, type FireworkVariant } from './types';
4
+
5
+ function between(rng: () => number, min: number, max: number): number {
6
+ return min + rng() * (max - min);
7
+ }
8
+
9
+ /**
10
+ * Creates an array of {@link Explosion} particles for the given firework variant.
11
+ * Use this to fire a fully formed explosion burst in your own render loop without
12
+ * needing a {@link Fireworks} instance.
13
+ *
14
+ * @param variant - The firework variant to create.
15
+ * @param position - The center position of the explosion in canvas pixels.
16
+ * @param hue - Base hue in degrees (0–360).
17
+ * @param options - Optional overrides for `lineWidth` (default `5`) and `scale` (default `1`).
18
+ * @param rng - RNG function returning values in [0, 1). Defaults to `Math.random`.
19
+ */
20
+ export function createExplosion(
21
+ variant: FireworkVariant,
22
+ position: Point,
23
+ hue: number,
24
+ options: { lineWidth?: number; scale?: number } = {},
25
+ rng: () => number = Math.random
26
+ ): Explosion[] {
27
+ const lineWidth = options.lineWidth ?? 5;
28
+ const scale = options.scale ?? 1;
29
+ const explosions: Explosion[] = [];
30
+
31
+ switch (variant) {
32
+ case 'saturn':
33
+ createSaturn(explosions, position, hue, lineWidth, scale, rng);
34
+ break;
35
+ case 'dahlia':
36
+ createDahlia(explosions, position, hue, lineWidth, scale, rng);
37
+ break;
38
+ case 'heart':
39
+ createHeart(explosions, position, hue, lineWidth, scale, rng);
40
+ break;
41
+ case 'spiral':
42
+ createSpiral(explosions, position, hue, lineWidth, scale, rng);
43
+ break;
44
+ case 'flower':
45
+ createFlower(explosions, position, hue, lineWidth, scale, rng);
46
+ break;
47
+ case 'concentric':
48
+ createConcentric(explosions, position, hue, lineWidth, scale, rng);
49
+ break;
50
+ default: {
51
+ const type: ExplosionType = variant;
52
+ const config = EXPLOSION_CONFIGS[type];
53
+ const count = Math.floor(between(rng, config.particleCount[0], config.particleCount[1]));
54
+ const effectiveHue = type === 'brocade' ? between(rng, 35, 50) : hue;
55
+
56
+ for (let i = 0; i < count; i++) {
57
+ let angle: number | undefined;
58
+ let speed: number | undefined;
59
+
60
+ if (type === 'ring') {
61
+ angle = (i / count) * Math.PI * 2;
62
+ speed = between(rng, config.speed[0], config.speed[1]) * 0.5 + config.speed[0] * 0.5;
63
+ } else if (type === 'palm' || type === 'horsetail') {
64
+ const spread = type === 'horsetail' ? Math.PI / 8 : Math.PI / 5;
65
+ angle = -Math.PI / 2 + between(rng, -spread, spread);
66
+ }
67
+
68
+ explosions.push(new Explosion(position, effectiveHue, lineWidth, type, scale, angle, speed));
69
+ }
70
+ }
71
+ }
72
+
73
+ return explosions;
74
+ }
75
+
76
+ function createSaturn(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
77
+ const velocity = between(rng, 4, 6);
78
+ const shellCount = Math.floor(between(rng, 25, 35));
79
+
80
+ for (let i = 0; i < shellCount; i++) {
81
+ const rad = (i / shellCount) * Math.PI * 2;
82
+
83
+ explosions.push(new Explosion(
84
+ position, hue, lineWidth, 'peony', scale,
85
+ rad + between(rng, -0.05, 0.05),
86
+ velocity + between(rng, -0.25, 0.25)
87
+ ));
88
+ }
89
+
90
+ const fillCount = Math.floor(between(rng, 40, 60));
91
+
92
+ for (let i = 0; i < fillCount; i++) {
93
+ explosions.push(new Explosion(
94
+ position, hue, lineWidth, 'peony', scale,
95
+ between(rng, 0, Math.PI * 2),
96
+ velocity * between(rng, 0, 1)
97
+ ));
98
+ }
99
+
100
+ const ringRotation = between(rng, 0, Math.PI * 2);
101
+ const ringCount = Math.floor(between(rng, 40, 55));
102
+ const ringVx = velocity * between(rng, 2, 3);
103
+ const ringVy = velocity * 0.6;
104
+
105
+ for (let i = 0; i < ringCount; i++) {
106
+ const rad = (i / ringCount) * Math.PI * 2;
107
+ const cx = Math.cos(rad) * ringVx + between(rng, -0.25, 0.25);
108
+ const cy = Math.sin(rad) * ringVy + between(rng, -0.25, 0.25);
109
+ const cosR = Math.cos(ringRotation);
110
+ const sinR = Math.sin(ringRotation);
111
+ const vx = cx * cosR - cy * sinR;
112
+ const vy = cx * sinR + cy * cosR;
113
+ const vz = Math.sin(rad) * velocity * 0.8;
114
+
115
+ explosions.push(new Explosion(
116
+ position, hue + 60, lineWidth, 'ring', scale,
117
+ Math.atan2(vy, vx),
118
+ Math.sqrt(vx * vx + vy * vy),
119
+ vz
120
+ ));
121
+ }
122
+ }
123
+
124
+ function createDahlia(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
125
+ const petalCount = Math.floor(between(rng, 6, 9));
126
+ const particlesPerPetal = Math.floor(between(rng, 8, 12));
127
+
128
+ for (let petal = 0; petal < petalCount; petal++) {
129
+ const baseAngle = (petal / petalCount) * Math.PI * 2;
130
+ const petalHue = hue + (petal % 2 === 0 ? 25 : -25);
131
+
132
+ for (let i = 0; i < particlesPerPetal; i++) {
133
+ explosions.push(new Explosion(
134
+ position, petalHue, lineWidth, 'dahlia', scale,
135
+ baseAngle + between(rng, -0.3, 0.3)
136
+ ));
137
+ }
138
+ }
139
+ }
140
+
141
+ function createHeart(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
142
+ const velocity = between(rng, 3, 5);
143
+ const count = Math.floor(between(rng, 60, 80));
144
+ const rotation = between(rng, -0.3, 0.3);
145
+ const cosR = Math.cos(rotation);
146
+ const sinR = Math.sin(rotation);
147
+
148
+ for (let i = 0; i < count; i++) {
149
+ const t = (i / count) * Math.PI * 2;
150
+ const hx = 16 * Math.pow(Math.sin(t), 3);
151
+ const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
152
+ const s = velocity / 16;
153
+ const vx = hx * s;
154
+ const vy = hy * s;
155
+ const rvx = vx * cosR - vy * sinR;
156
+ const rvy = vx * sinR + vy * cosR;
157
+
158
+ explosions.push(new Explosion(
159
+ position, hue, lineWidth, 'heart', scale,
160
+ Math.atan2(rvy, rvx),
161
+ Math.max(0.1, Math.sqrt(rvx * rvx + rvy * rvy) + between(rng, -0.15, 0.15))
162
+ ));
163
+ }
164
+ }
165
+
166
+ function createSpiral(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
167
+ const arms = Math.floor(between(rng, 3, 5));
168
+ const particlesPerArm = Math.floor(between(rng, 15, 20));
169
+ const twist = between(rng, 2, 3.5);
170
+ const baseRotation = between(rng, 0, Math.PI * 2);
171
+
172
+ for (let arm = 0; arm < arms; arm++) {
173
+ const baseAngle = baseRotation + (arm / arms) * Math.PI * 2;
174
+ const armHue = hue + arm * (360 / arms / 3);
175
+
176
+ for (let i = 0; i < particlesPerArm; i++) {
177
+ const progress = i / particlesPerArm;
178
+
179
+ explosions.push(new Explosion(
180
+ position, armHue, lineWidth, 'spiral', scale,
181
+ baseAngle + progress * twist,
182
+ 2 + progress * 8 + between(rng, -0.3, 0.3)
183
+ ));
184
+ }
185
+ }
186
+ }
187
+
188
+ function createFlower(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
189
+ const velocity = between(rng, 4, 7);
190
+ const count = Math.floor(between(rng, 70, 90));
191
+ const petals = Math.floor(between(rng, 2, 4));
192
+ const rotation = between(rng, 0, Math.PI * 2);
193
+
194
+ for (let i = 0; i < count; i++) {
195
+ const t = (i / count) * Math.PI * 2;
196
+ const r = Math.abs(Math.cos(petals * t));
197
+ const speed = velocity * r;
198
+
199
+ if (speed < 0.3) {
200
+ continue;
201
+ }
202
+
203
+ explosions.push(new Explosion(
204
+ position, hue + between(rng, -15, 15), lineWidth, 'flower', scale,
205
+ t + rotation,
206
+ speed + between(rng, -0.2, 0.2)
207
+ ));
208
+ }
209
+ }
210
+
211
+ function createConcentric(explosions: Explosion[], position: Point, hue: number, lineWidth: number, scale: number, rng: () => number): void {
212
+ const outerCount = Math.floor(between(rng, 35, 50));
213
+ const outerSpeed = between(rng, 7, 10);
214
+
215
+ for (let i = 0; i < outerCount; i++) {
216
+ const angle = (i / outerCount) * Math.PI * 2;
217
+
218
+ explosions.push(new Explosion(
219
+ position, hue, lineWidth, 'ring', scale,
220
+ angle + between(rng, -0.05, 0.05),
221
+ outerSpeed + between(rng, -0.25, 0.25)
222
+ ));
223
+ }
224
+
225
+ const innerCount = Math.floor(between(rng, 25, 35));
226
+ const innerSpeed = between(rng, 3, 5);
227
+
228
+ for (let i = 0; i < innerCount; i++) {
229
+ const angle = (i / innerCount) * Math.PI * 2;
230
+
231
+ explosions.push(new Explosion(
232
+ position, hue + 120, lineWidth, 'ring', scale,
233
+ angle + between(rng, -0.05, 0.05),
234
+ innerSpeed + between(rng, -0.25, 0.25)
235
+ ));
236
+ }
237
+ }
@@ -77,7 +77,7 @@ export class Explosion {
77
77
  return false;
78
78
  }
79
79
 
80
- if (this.#alpha <= this.#decay * 3) {
80
+ if (this.#alpha <= 0.4) {
81
81
  this.#hasCrackled = true;
82
82
  return true;
83
83
  }
@@ -147,21 +147,21 @@ export class Explosion {
147
147
  ctx.restore();
148
148
  }
149
149
 
150
- tick(): void {
150
+ tick(dt: number): void {
151
151
  this.#trail.pop();
152
152
  this.#trail.unshift({...this.#position});
153
153
 
154
- this.#speed *= this.#config.friction;
155
- this.#vz *= this.#config.friction;
154
+ this.#speed *= Math.pow(this.#config.friction, dt);
155
+ this.#vz *= Math.pow(this.#config.friction, dt);
156
156
 
157
- this.#position.x += Math.cos(this.#angle) * this.#speed;
158
- this.#position.y += Math.sin(this.#angle) * this.#speed + this.#config.gravity;
159
- this.#z += this.#vz;
157
+ this.#position.x += Math.cos(this.#angle) * this.#speed * dt;
158
+ this.#position.y += (Math.sin(this.#angle) * this.#speed + this.#config.gravity) * dt;
159
+ this.#z += this.#vz * dt;
160
160
 
161
161
  this.#depthScale = PERSPECTIVE / (PERSPECTIVE + this.#z);
162
162
 
163
- this.#alpha -= this.#decay;
164
- this.#sparkleTimer++;
163
+ this.#alpha -= this.#decay * dt;
164
+ this.#sparkleTimer += dt;
165
165
  }
166
166
 
167
167
  #drawShape(ctx: CanvasRenderingContext2D, ds: number, alpha: number): void {
@@ -87,18 +87,18 @@ export class Firework extends EventTarget {
87
87
  ctx.restore();
88
88
  }
89
89
 
90
- tick(): void {
90
+ tick(dt: number): void {
91
91
  this.#trail.pop();
92
92
  this.#trail.unshift({...this.#position});
93
93
 
94
- this.#speed *= this.#acceleration;
94
+ this.#speed *= Math.pow(this.#acceleration, dt);
95
95
 
96
96
  const vx = Math.cos(this.#angle) * this.#speed;
97
97
  const vy = Math.sin(this.#angle) * this.#speed;
98
98
 
99
99
  this.#distanceTraveled = distance(this.#startPosition, {
100
- x: this.#position.x + vx,
101
- y: this.#position.y + vy
100
+ x: this.#position.x + vx * dt,
101
+ y: this.#position.y + vy * dt
102
102
  });
103
103
 
104
104
  if (this.#distanceTraveled >= this.#distance) {
@@ -106,12 +106,13 @@ export class Firework extends EventTarget {
106
106
  return;
107
107
  }
108
108
 
109
- this.#position.x += vx;
110
- this.#position.y += vy;
109
+ this.#position.x += vx * dt;
110
+ this.#position.y += vy * dt;
111
111
 
112
- this.#sparkTimer++;
112
+ this.#sparkTimer += dt;
113
113
 
114
- if (this.#sparkTimer % 3 === 0) {
114
+ if (this.#sparkTimer >= 3) {
115
+ this.#sparkTimer -= 3;
115
116
  this.#pendingSparks.push(new Spark(
116
117
  this.#position,
117
118
  this.#hue,
@@ -1,3 +1,19 @@
1
- export * from './simulation';
2
- export { FIREWORK_VARIANTS } from './types';
3
- export type { ExplosionType, FireworkSimulationConfig, FireworkVariant, ParticleShape } from './types';
1
+ import { Fireworks } from './layer';
2
+ import type { FireworksConfig, FireworkVariant } from './types';
3
+ import type { Point } from '../point';
4
+ import type { Effect } from '../effect';
5
+
6
+ export interface FireworksInstance extends Effect<FireworksConfig> {
7
+ launch(variant: FireworkVariant, position?: Point): void;
8
+ }
9
+
10
+ export function createFireworks(config?: FireworksConfig): FireworksInstance {
11
+ return new Fireworks(config) as FireworksInstance;
12
+ }
13
+
14
+ export { createExplosion } from './create-explosion';
15
+ export { Explosion } from './explosion';
16
+ export { Firework } from './firework';
17
+ export { Spark } from './spark';
18
+ export { EXPLOSION_CONFIGS, FIREWORK_VARIANTS } from './types';
19
+ export type { ExplosionConfig, ExplosionType, FireworksConfig, FireworkVariant, ParticleShape } from './types';
@@ -0,0 +1,203 @@
1
+ import { Effect } from '../effect';
2
+ import type { Point } from '../point';
3
+ import { MULBERRY } from './consts';
4
+ import { createExplosion } from './create-explosion';
5
+ import { Explosion } from './explosion';
6
+ import { Firework } from './firework';
7
+ import { Spark } from './spark';
8
+ import { FIREWORK_VARIANTS, type FireworksConfig, type FireworkVariant } from './types';
9
+
10
+ export class Fireworks extends Effect<FireworksConfig> {
11
+ #explosions: Explosion[] = [];
12
+ #fireworks: Firework[] = [];
13
+ #sparks: Spark[] = [];
14
+ #hue: number = 120;
15
+ #spawnTimer: number = 0;
16
+ #positionRandom = MULBERRY.fork();
17
+ #autoSpawn: boolean;
18
+ #variants: FireworkVariant[];
19
+ readonly #baseSize: number;
20
+ #scale: number;
21
+ readonly #tailWidth: number;
22
+ #width: number = 960;
23
+ #height: number = 540;
24
+
25
+ constructor(config: FireworksConfig = {}) {
26
+ super();
27
+
28
+ const scale = config.scale ?? 1;
29
+ this.#autoSpawn = config.autoSpawn ?? true;
30
+ this.#variants = config.variants?.length ? [...config.variants] : [...FIREWORK_VARIANTS];
31
+ this.#baseSize = 5 * scale;
32
+ this.#scale = scale;
33
+ this.#tailWidth = 2 * scale;
34
+ }
35
+
36
+ onResize(width: number, height: number): void {
37
+ this.#width = width;
38
+ this.#height = height;
39
+ }
40
+
41
+ launch(variant: FireworkVariant, position?: Point): void {
42
+ const pos = position ?? {x: this.#width / 2, y: this.#height * 0.4};
43
+ this.#hue = MULBERRY.nextBetween(0, 360);
44
+ this.#spawnExplosion(pos, this.#hue, variant);
45
+ }
46
+
47
+ configure(config: Partial<FireworksConfig>): void {
48
+ if (config.scale !== undefined) {
49
+ this.#scale = config.scale;
50
+ }
51
+ if (config.autoSpawn !== undefined) {
52
+ this.#autoSpawn = config.autoSpawn;
53
+ }
54
+ if (Array.isArray(config.variants) && config.variants.length > 0) {
55
+ this.#variants = [...config.variants];
56
+ }
57
+ }
58
+
59
+ tick(dt: number, width: number, height: number): void {
60
+ this.#width = width;
61
+ this.#height = height;
62
+ this.#spawnTimer += dt;
63
+
64
+ const isSmall = innerWidth < 991;
65
+ const spawnInterval = isSmall ? 60 : 30;
66
+
67
+ if (this.#autoSpawn && this.#fireworks.length < 6 && this.#spawnTimer >= spawnInterval) {
68
+ this.#spawnTimer -= spawnInterval;
69
+ let count = MULBERRY.nextBetween(1, 100) < 10 ? 2 : 1;
70
+
71
+ while (count--) {
72
+ this.#hue = MULBERRY.nextBetween(0, 360);
73
+ this.#createFirework();
74
+ }
75
+ }
76
+
77
+ for (const firework of this.#fireworks) {
78
+ firework.tick(dt);
79
+ const collected = firework.collectSparks();
80
+ for (let i = 0; i < collected.length; i++) {
81
+ this.#sparks.push(collected[i]);
82
+ }
83
+ }
84
+
85
+ for (const explosion of this.#explosions) {
86
+ explosion.tick(dt);
87
+ }
88
+
89
+ for (const spark of this.#sparks) {
90
+ spark.tick(dt);
91
+ }
92
+
93
+ const newExplosions: Explosion[] = [];
94
+ const newSparks: Spark[] = [];
95
+
96
+ for (const explosion of this.#explosions) {
97
+ if (explosion.checkSplit()) {
98
+ for (let i = 0; i < 4; i++) {
99
+ const angle = explosion.angle + (Math.PI / 2) * i + Math.PI / 4;
100
+
101
+ newExplosions.push(new Explosion(
102
+ explosion.position,
103
+ explosion.hue,
104
+ this.#baseSize * 0.6,
105
+ 'peony',
106
+ this.#scale,
107
+ angle,
108
+ MULBERRY.nextBetween(3, 6)
109
+ ));
110
+ }
111
+ }
112
+
113
+ if (explosion.checkCrackle()) {
114
+ for (let j = 0; j < 14; j++) {
115
+ const angle = MULBERRY.nextBetween(0, Math.PI * 2);
116
+ const speed = MULBERRY.nextBetween(3, 8);
117
+
118
+ newSparks.push(new Spark(
119
+ explosion.position,
120
+ explosion.hue + MULBERRY.nextBetween(-30, 30),
121
+ Math.cos(angle) * speed,
122
+ Math.sin(angle) * speed
123
+ ));
124
+ }
125
+ }
126
+ }
127
+
128
+ this.#explosions.push(...newExplosions);
129
+ this.#sparks.push(...newSparks);
130
+
131
+ let aliveExplosions = 0;
132
+ for (let i = 0; i < this.#explosions.length; i++) {
133
+ if (!this.#explosions[i].isDead) {
134
+ this.#explosions[aliveExplosions++] = this.#explosions[i];
135
+ }
136
+ }
137
+ this.#explosions.length = aliveExplosions;
138
+
139
+ let aliveSparks = 0;
140
+ for (let i = 0; i < this.#sparks.length; i++) {
141
+ if (!this.#sparks[i].isDead) {
142
+ this.#sparks[aliveSparks++] = this.#sparks[i];
143
+ }
144
+ }
145
+ this.#sparks.length = aliveSparks;
146
+ }
147
+
148
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
149
+ if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
150
+ ctx.canvas.width = width;
151
+ ctx.canvas.height = height;
152
+ }
153
+
154
+ ctx.globalCompositeOperation = 'lighter';
155
+
156
+ for (const spark of this.#sparks) {
157
+ spark.draw(ctx);
158
+ }
159
+
160
+ for (const explosion of this.#explosions) {
161
+ explosion.draw(ctx);
162
+ }
163
+
164
+ for (const firework of this.#fireworks) {
165
+ firework.draw(ctx);
166
+ }
167
+
168
+ ctx.globalCompositeOperation = 'source-over';
169
+ }
170
+
171
+ #spawnExplosion(position: Point, hue: number, variant?: FireworkVariant): void {
172
+ const selected = variant ?? this.#pickVariant();
173
+ const rng = () => MULBERRY.nextBetween(0, 1);
174
+ this.#explosions.push(...createExplosion(selected, position, hue, {lineWidth: this.#baseSize, scale: this.#scale}, rng));
175
+ }
176
+
177
+ #createFirework(position?: Point): void {
178
+ const hue = this.#hue;
179
+ const targetX = position?.x || this.#positionRandom.nextBetween(this.#width * .1, this.#width * .9);
180
+ const targetY = position?.y || this.#height * .1 + this.#positionRandom.nextBetween(0, this.#height * .5);
181
+ const startX = this.#width * 0.3 + this.#positionRandom.nextBetween(0, this.#width * 0.4);
182
+
183
+ const firework = new Firework(
184
+ {x: startX, y: this.#height},
185
+ {x: targetX, y: targetY},
186
+ hue,
187
+ this.#tailWidth,
188
+ this.#baseSize
189
+ );
190
+
191
+ firework.addEventListener('remove', () => {
192
+ this.#fireworks.splice(this.#fireworks.indexOf(firework), 1);
193
+ this.#spawnExplosion(firework.position, hue);
194
+ }, {once: true});
195
+
196
+ this.#fireworks.push(firework);
197
+ }
198
+
199
+ #pickVariant(): FireworkVariant {
200
+ const index = Math.floor(MULBERRY.nextBetween(0, this.#variants.length));
201
+ return this.#variants[index];
202
+ }
203
+ }
@@ -8,7 +8,7 @@ export class Spark {
8
8
  readonly #size: number;
9
9
  readonly #decay: number;
10
10
  readonly #friction: number = 0.94;
11
- readonly #gravity: number = 0.6;
11
+ readonly #gravity: number = 0.3;
12
12
  #alpha: number = 1;
13
13
 
14
14
  get isDead(): boolean {
@@ -26,7 +26,7 @@ export class Spark {
26
26
  this.#decay = MULBERRY.nextBetween(0.03, 0.08);
27
27
  this.#velocity = {
28
28
  x: velocityX + MULBERRY.nextBetween(-1.5, 1.5),
29
- y: velocityY + MULBERRY.nextBetween(-2, 0.5)
29
+ y: velocityY + MULBERRY.nextBetween(-2, 2)
30
30
  };
31
31
  }
32
32
 
@@ -37,14 +37,14 @@ export class Spark {
37
37
  ctx.fill();
38
38
  }
39
39
 
40
- tick(): void {
41
- this.#velocity.x *= this.#friction;
42
- this.#velocity.y *= this.#friction;
43
- this.#velocity.y += this.#gravity;
40
+ tick(dt: number): void {
41
+ this.#velocity.x *= Math.pow(this.#friction, dt);
42
+ this.#velocity.y *= Math.pow(this.#friction, dt);
43
+ this.#velocity.y += this.#gravity * dt;
44
44
 
45
- this.#position.x += this.#velocity.x;
46
- this.#position.y += this.#velocity.y;
45
+ this.#position.x += this.#velocity.x * dt;
46
+ this.#position.y += this.#velocity.y * dt;
47
47
 
48
- this.#alpha -= this.#decay;
48
+ this.#alpha -= this.#decay * dt;
49
49
  }
50
50
  }
@@ -21,10 +21,10 @@ export interface ExplosionConfig {
21
21
  readonly glowSize: number;
22
22
  }
23
23
 
24
- export interface FireworkSimulationConfig {
24
+ export interface FireworksConfig {
25
25
  readonly scale?: number;
26
26
  readonly autoSpawn?: boolean;
27
- readonly canvasOptions?: CanvasRenderingContext2DSettings;
27
+ readonly variants?: FireworkVariant[];
28
28
  }
29
29
 
30
30
  export const FIREWORK_VARIANTS: FireworkVariant[] = [
@@ -0,0 +1,13 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const GLITTER_COLORS: string[] = [
6
+ '#ffd700', // gold
7
+ '#c0c0c0', // silver
8
+ '#ff69b4', // pink
9
+ '#00bfff', // sky blue
10
+ '#ff4500', // orange-red
11
+ '#7fff00', // chartreuse
12
+ '#9370db' // medium purple
13
+ ];
@@ -0,0 +1,9 @@
1
+ import { Glitter } from './layer';
2
+ import type { GlitterConfig } from './types';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createGlitter(config?: GlitterConfig): Effect<GlitterConfig> {
6
+ return new Glitter(config);
7
+ }
8
+
9
+ export type { GlitterConfig, FallingGlitter, SettledGlitter } from './types';