@basmilius/sparkle 2.3.0 → 2.5.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 (126) hide show
  1. package/dist/index.d.mts +637 -1
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +6964 -2577
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/balloons/layer.ts +4 -3
  7. package/src/black-hole/consts.ts +3 -0
  8. package/src/black-hole/index.ts +10 -0
  9. package/src/black-hole/layer.ts +193 -0
  10. package/src/black-hole/types.ts +8 -0
  11. package/src/boids/consts.ts +8 -0
  12. package/src/boids/index.ts +9 -0
  13. package/src/boids/layer.ts +245 -0
  14. package/src/boids/types.ts +7 -0
  15. package/src/butterflies/consts.ts +3 -0
  16. package/src/butterflies/index.ts +9 -0
  17. package/src/butterflies/layer.ts +246 -0
  18. package/src/butterflies/types.ts +23 -0
  19. package/src/caustics/consts.ts +3 -0
  20. package/src/caustics/index.ts +9 -0
  21. package/src/caustics/layer.ts +107 -0
  22. package/src/clouds/consts.ts +3 -0
  23. package/src/clouds/index.ts +9 -0
  24. package/src/clouds/layer.ts +167 -0
  25. package/src/clouds/types.ts +9 -0
  26. package/src/confetti/layer.ts +3 -2
  27. package/src/constellation/consts.ts +3 -0
  28. package/src/constellation/index.ts +10 -0
  29. package/src/constellation/layer.ts +256 -0
  30. package/src/constellation/types.ts +11 -0
  31. package/src/coral-reef/consts.ts +3 -0
  32. package/src/coral-reef/index.ts +10 -0
  33. package/src/coral-reef/layer.ts +276 -0
  34. package/src/coral-reef/types.ts +31 -0
  35. package/src/crystallization/consts.ts +3 -0
  36. package/src/crystallization/index.ts +10 -0
  37. package/src/crystallization/layer.ts +318 -0
  38. package/src/crystallization/types.ts +25 -0
  39. package/src/digital-rain/consts.ts +7 -0
  40. package/src/digital-rain/index.ts +10 -0
  41. package/src/digital-rain/layer.ts +195 -0
  42. package/src/digital-rain/types.ts +10 -0
  43. package/src/donuts/layer.ts +5 -3
  44. package/src/glitch/consts.ts +3 -0
  45. package/src/glitch/index.ts +9 -0
  46. package/src/glitch/layer.ts +231 -0
  47. package/src/glitch/types.ts +28 -0
  48. package/src/gradient-flow/consts.ts +3 -0
  49. package/src/gradient-flow/index.ts +9 -0
  50. package/src/gradient-flow/layer.ts +134 -0
  51. package/src/gradient-flow/types.ts +8 -0
  52. package/src/hologram/consts.ts +5 -0
  53. package/src/hologram/index.ts +9 -0
  54. package/src/hologram/layer.ts +205 -0
  55. package/src/hologram/types.ts +20 -0
  56. package/src/hyper-space/consts.ts +3 -0
  57. package/src/hyper-space/index.ts +10 -0
  58. package/src/hyper-space/layer.ts +167 -0
  59. package/src/hyper-space/types.ts +8 -0
  60. package/src/index.ts +29 -0
  61. package/src/interference/consts.ts +9 -0
  62. package/src/interference/index.ts +9 -0
  63. package/src/interference/layer.ts +129 -0
  64. package/src/kaleidoscope/consts.ts +12 -0
  65. package/src/kaleidoscope/index.ts +9 -0
  66. package/src/kaleidoscope/layer.ts +213 -0
  67. package/src/kaleidoscope/types.ts +19 -0
  68. package/src/lanterns/layer.ts +3 -2
  69. package/src/lava/consts.ts +3 -0
  70. package/src/lava/index.ts +9 -0
  71. package/src/lava/layer.ts +152 -0
  72. package/src/lava/types.ts +13 -0
  73. package/src/leaves/layer.ts +3 -2
  74. package/src/murmuration/consts.ts +3 -0
  75. package/src/murmuration/index.ts +10 -0
  76. package/src/murmuration/layer.ts +279 -0
  77. package/src/murmuration/types.ts +7 -0
  78. package/src/nebula/consts.ts +3 -0
  79. package/src/nebula/index.ts +10 -0
  80. package/src/nebula/layer.ts +150 -0
  81. package/src/nebula/types.ts +20 -0
  82. package/src/neon/consts.ts +5 -0
  83. package/src/neon/index.ts +9 -0
  84. package/src/neon/layer.ts +213 -0
  85. package/src/neon/types.ts +18 -0
  86. package/src/petals/layer.ts +3 -2
  87. package/src/pollen/consts.ts +3 -0
  88. package/src/pollen/index.ts +10 -0
  89. package/src/pollen/layer.ts +181 -0
  90. package/src/pollen/types.ts +10 -0
  91. package/src/popcorn/consts.ts +3 -0
  92. package/src/popcorn/index.ts +10 -0
  93. package/src/popcorn/layer.ts +218 -0
  94. package/src/popcorn/types.ts +13 -0
  95. package/src/portal/consts.ts +3 -0
  96. package/src/portal/index.ts +10 -0
  97. package/src/portal/layer.ts +251 -0
  98. package/src/portal/types.ts +10 -0
  99. package/src/pulse-grid/consts.ts +3 -0
  100. package/src/pulse-grid/index.ts +10 -0
  101. package/src/pulse-grid/layer.ts +185 -0
  102. package/src/pulse-grid/types.ts +8 -0
  103. package/src/roots/consts.ts +3 -0
  104. package/src/roots/index.ts +9 -0
  105. package/src/roots/layer.ts +218 -0
  106. package/src/roots/types.ts +23 -0
  107. package/src/smoke/consts.ts +3 -0
  108. package/src/smoke/index.ts +9 -0
  109. package/src/smoke/layer.ts +182 -0
  110. package/src/smoke/types.ts +14 -0
  111. package/src/snow/layer.ts +3 -2
  112. package/src/topography/consts.ts +3 -0
  113. package/src/topography/index.ts +9 -0
  114. package/src/topography/layer.ts +141 -0
  115. package/src/tornado/consts.ts +3 -0
  116. package/src/tornado/index.ts +10 -0
  117. package/src/tornado/layer.ts +271 -0
  118. package/src/tornado/types.ts +22 -0
  119. package/src/volcano/consts.ts +3 -0
  120. package/src/volcano/index.ts +10 -0
  121. package/src/volcano/layer.ts +261 -0
  122. package/src/volcano/types.ts +10 -0
  123. package/src/voronoi/consts.ts +3 -0
  124. package/src/voronoi/index.ts +10 -0
  125. package/src/voronoi/layer.ts +197 -0
  126. package/src/voronoi/types.ts +7 -0
@@ -0,0 +1,271 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { TornadoDebris, TornadoParticle } from './types';
4
+
5
+ export interface TornadoConfig {
6
+ readonly speed?: number;
7
+ readonly debris?: number;
8
+ readonly width?: number;
9
+ readonly intensity?: number;
10
+ readonly color?: string;
11
+ readonly scale?: number;
12
+ }
13
+
14
+ export class Tornado extends Effect<TornadoConfig> {
15
+ readonly #scale: number;
16
+ #speed: number;
17
+ #intensity: number;
18
+ readonly #funnelWidth: number;
19
+ readonly #particleCount: number;
20
+ readonly #debrisCount: number;
21
+ #time: number = 0;
22
+ #particles: TornadoParticle[] = [];
23
+ #debris: TornadoDebris[] = [];
24
+ #colorR: number;
25
+ #colorG: number;
26
+ #colorB: number;
27
+
28
+ constructor(config: TornadoConfig = {}) {
29
+ super();
30
+
31
+ this.#scale = config.scale ?? 1;
32
+ this.#speed = config.speed ?? 1;
33
+ this.#funnelWidth = config.width ?? 0.3;
34
+ this.#intensity = config.intensity ?? 1;
35
+
36
+ let particleCount = 400;
37
+ let debrisCount = config.debris ?? 40;
38
+
39
+ if (innerWidth < 991) {
40
+ particleCount = Math.floor(particleCount / 2);
41
+ debrisCount = Math.floor(debrisCount / 2);
42
+ }
43
+
44
+ this.#particleCount = particleCount;
45
+ this.#debrisCount = debrisCount;
46
+
47
+ const {r, g, b} = this.#parseColor(config.color ?? '#8B7355');
48
+ this.#colorR = r;
49
+ this.#colorG = g;
50
+ this.#colorB = b;
51
+
52
+ for (let i = 0; i < this.#particleCount; ++i) {
53
+ this.#particles.push(this.#createParticle());
54
+ }
55
+
56
+ for (let i = 0; i < this.#debrisCount; ++i) {
57
+ this.#debris.push(this.#createDebris(true));
58
+ }
59
+ }
60
+
61
+ configure(config: Partial<TornadoConfig>): void {
62
+ if (config.speed !== undefined) {
63
+ this.#speed = config.speed;
64
+ }
65
+ if (config.intensity !== undefined) {
66
+ this.#intensity = config.intensity;
67
+ }
68
+ }
69
+
70
+ tick(dt: number, width: number, height: number): void {
71
+ this.#time += 0.015 * dt * this.#speed;
72
+
73
+ for (const particle of this.#particles) {
74
+ const speedMultiplier = 1 + (1 - particle.height) * 2;
75
+ particle.angle += particle.speed * speedMultiplier * this.#intensity * 0.06 * dt;
76
+ particle.height += 0.0008 * dt * this.#speed * (0.5 + particle.layer * 0.5);
77
+
78
+ if (particle.height > 1.1) {
79
+ particle.height = -0.05;
80
+ particle.angle = MULBERRY.next() * Math.PI * 2;
81
+ particle.radiusOffset = 0.6 + MULBERRY.next() * 0.8;
82
+ }
83
+ }
84
+
85
+ const baseY = height * 0.92;
86
+ const swayX = this.#getSway(width);
87
+ const baseCX = width * 0.5 + swayX * 0.3;
88
+
89
+ let alive = 0;
90
+
91
+ for (let i = 0; i < this.#debris.length; ++i) {
92
+ const debris = this.#debris[i];
93
+ debris.life += dt;
94
+
95
+ if (debris.life > debris.maxLife) {
96
+ this.#debris[alive++] = this.#createDebris(false);
97
+ continue;
98
+ }
99
+
100
+ debris.vy += 0.12 * dt;
101
+ debris.vx *= 0.995;
102
+ debris.vy *= 0.995;
103
+
104
+ const distToCenter = debris.x - baseCX;
105
+ debris.vx -= distToCenter * 0.0003 * dt * this.#intensity;
106
+ debris.vy -= 0.15 * dt * this.#intensity;
107
+
108
+ debris.x += debris.vx * dt;
109
+ debris.y += debris.vy * dt;
110
+ debris.rotation += debris.rotationSpeed * dt;
111
+ debris.opacity = 1 - debris.life / debris.maxLife;
112
+
113
+ if (debris.y > baseY + 10) {
114
+ debris.y = baseY + 10;
115
+ debris.vy = -Math.abs(debris.vy) * 0.3;
116
+ }
117
+
118
+ this.#debris[alive++] = debris;
119
+ }
120
+
121
+ this.#debris.length = alive;
122
+
123
+ while (this.#debris.length < this.#debrisCount) {
124
+ this.#debris.push(this.#createDebris(false));
125
+ }
126
+ }
127
+
128
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
129
+ const baseY = height * 0.92;
130
+ const topY = height * 0.05;
131
+ const funnelHeight = baseY - topY;
132
+ const swayX = this.#getSway(width);
133
+ const baseCX = width * 0.5 + swayX * 0.3;
134
+ const topCX = width * 0.5 + swayX;
135
+ const baseRadius = this.#funnelWidth * width * 0.12 * this.#scale;
136
+ const topRadius = this.#funnelWidth * width * 0.5 * this.#scale;
137
+ const cr = this.#colorR;
138
+ const cg = this.#colorG;
139
+ const cb = this.#colorB;
140
+
141
+ this.#drawDustCloud(ctx, baseCX, baseY, width, cr, cg, cb);
142
+
143
+ for (const particle of this.#particles) {
144
+ if (particle.height < 0 || particle.height > 1) {
145
+ continue;
146
+ }
147
+
148
+ const t = particle.height;
149
+ const easeT = t * t;
150
+
151
+ const cx = baseCX + (topCX - baseCX) * easeT;
152
+ const cy = baseY - funnelHeight * t;
153
+ const radius = baseRadius + (topRadius - baseRadius) * easeT;
154
+
155
+ const orbitRadius = radius * particle.radiusOffset;
156
+ const px = cx + Math.cos(particle.angle) * orbitRadius;
157
+ const py = cy + Math.sin(particle.angle * 0.4) * radius * 0.08;
158
+
159
+ const depthFactor = (Math.sin(particle.angle) + 1) * 0.5;
160
+ const heightFade = t < 0.05 ? t / 0.05 : (t > 0.95 ? (1 - t) / 0.05 : 1);
161
+ const layerAlpha = particle.layer === 0 ? 0.3 : (particle.layer === 1 ? 0.2 : 0.12);
162
+ const alpha = particle.opacity * heightFade * (0.4 + depthFactor * 0.6) * layerAlpha * this.#intensity;
163
+
164
+ if (alpha < 0.01) {
165
+ continue;
166
+ }
167
+
168
+ const size = particle.size * this.#scale * (0.6 + t * 0.8);
169
+
170
+ ctx.globalAlpha = alpha;
171
+ ctx.fillStyle = `rgb(${cr + (255 - cr) * depthFactor * 0.3 | 0}, ${cg + (255 - cg) * depthFactor * 0.2 | 0}, ${cb + (255 - cb) * depthFactor * 0.15 | 0})`;
172
+ ctx.beginPath();
173
+ ctx.arc(px, py, size, 0, Math.PI * 2);
174
+ ctx.fill();
175
+ }
176
+
177
+ ctx.fillStyle = `rgb(${cr}, ${cg}, ${cb})`;
178
+ ctx.strokeStyle = `rgb(${cr * 0.6 | 0}, ${cg * 0.6 | 0}, ${cb * 0.6 | 0})`;
179
+ ctx.lineCap = 'round';
180
+
181
+ for (const debris of this.#debris) {
182
+ if (debris.opacity < 0.05) {
183
+ continue;
184
+ }
185
+
186
+ const size = debris.size * this.#scale;
187
+ ctx.globalAlpha = debris.opacity * 0.8;
188
+ ctx.lineWidth = Math.max(1, size * 0.5);
189
+
190
+ const cos = Math.cos(debris.rotation);
191
+ const sin = Math.sin(debris.rotation);
192
+ const hx = cos * size;
193
+ const hy = sin * size;
194
+
195
+ ctx.beginPath();
196
+ ctx.moveTo(debris.x - hx, debris.y - hy);
197
+ ctx.lineTo(debris.x + hx, debris.y + hy);
198
+ ctx.stroke();
199
+ }
200
+
201
+ ctx.globalAlpha = 1;
202
+ }
203
+
204
+ #drawDustCloud(ctx: CanvasRenderingContext2D, cx: number, baseY: number, _width: number, r: number, g: number, b: number): void {
205
+ const cloudRadius = this.#funnelWidth * 80 * this.#scale;
206
+
207
+ const gradient = ctx.createRadialGradient(
208
+ cx, baseY, 0,
209
+ cx, baseY, cloudRadius
210
+ );
211
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${0.15 * this.#intensity})`);
212
+ gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, ${0.06 * this.#intensity})`);
213
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
214
+
215
+ ctx.globalAlpha = 1;
216
+ ctx.fillStyle = gradient;
217
+ ctx.beginPath();
218
+ ctx.arc(cx, baseY, cloudRadius, 0, Math.PI * 2);
219
+ ctx.fill();
220
+ }
221
+
222
+ #getSway(width: number): number {
223
+ return Math.sin(this.#time * 0.8) * width * 0.06
224
+ + Math.sin(this.#time * 1.9 + 1.5) * width * 0.03
225
+ + Math.sin(this.#time * 3.1 + 0.7) * width * 0.01;
226
+ }
227
+
228
+ #createParticle(): TornadoParticle {
229
+ const layer = MULBERRY.next() < 0.4 ? 0 : (MULBERRY.next() < 0.6 ? 1 : 2);
230
+
231
+ return {
232
+ angle: MULBERRY.next() * Math.PI * 2,
233
+ height: MULBERRY.next(),
234
+ radiusOffset: layer === 0
235
+ ? (0.85 + MULBERRY.next() * 0.3)
236
+ : layer === 1
237
+ ? (0.5 + MULBERRY.next() * 0.5)
238
+ : (1.1 + MULBERRY.next() * 0.4),
239
+ speed: 0.5 + MULBERRY.next() * 0.8,
240
+ size: (1.5 + MULBERRY.next() * 3) * (layer === 2 ? 0.7 : 1),
241
+ opacity: 0.6 + MULBERRY.next() * 0.4,
242
+ layer
243
+ };
244
+ }
245
+
246
+ #createDebris(spread: boolean): TornadoDebris {
247
+ return {
248
+ x: 0,
249
+ y: 0,
250
+ vx: (MULBERRY.next() - 0.5) * 4 * this.#intensity,
251
+ vy: -MULBERRY.next() * 3 * this.#intensity,
252
+ size: 2 + MULBERRY.next() * 5,
253
+ rotation: MULBERRY.next() * Math.PI * 2,
254
+ rotationSpeed: (MULBERRY.next() - 0.5) * 0.15,
255
+ opacity: spread ? MULBERRY.next() : 1,
256
+ life: spread ? MULBERRY.next() * 120 : 0,
257
+ maxLife: 120 + MULBERRY.next() * 180
258
+ };
259
+ }
260
+
261
+ #parseColor(color: string): { r: number; g: number; b: number } {
262
+ const canvas = document.createElement('canvas');
263
+ canvas.width = 1;
264
+ canvas.height = 1;
265
+ const colorCtx = canvas.getContext('2d')!;
266
+ colorCtx.fillStyle = color;
267
+ colorCtx.fillRect(0, 0, 1, 1);
268
+ const data = colorCtx.getImageData(0, 0, 1, 1).data;
269
+ return {r: data[0], g: data[1], b: data[2]};
270
+ }
271
+ }
@@ -0,0 +1,22 @@
1
+ export type TornadoParticle = {
2
+ angle: number;
3
+ height: number;
4
+ radiusOffset: number;
5
+ speed: number;
6
+ size: number;
7
+ opacity: number;
8
+ layer: number;
9
+ };
10
+
11
+ export type TornadoDebris = {
12
+ x: number;
13
+ y: number;
14
+ vx: number;
15
+ vy: number;
16
+ size: number;
17
+ rotation: number;
18
+ rotationSpeed: number;
19
+ opacity: number;
20
+ life: number;
21
+ maxLife: number;
22
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,10 @@
1
+ import { Volcano } from './layer';
2
+ import type { VolcanoConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createVolcano(config?: VolcanoConfig): Effect<VolcanoConfig> {
6
+ return new Volcano(config);
7
+ }
8
+
9
+ export type { VolcanoConfig };
10
+ export type { VolcanoProjectile } from './types';
@@ -0,0 +1,261 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { VolcanoProjectile } from './types';
4
+
5
+ export interface VolcanoConfig {
6
+ readonly speed?: number;
7
+ readonly projectiles?: number;
8
+ readonly embers?: number;
9
+ readonly intensity?: number;
10
+ readonly color?: string;
11
+ readonly smokeColor?: string;
12
+ readonly scale?: number;
13
+ }
14
+
15
+ export class Volcano extends Effect<VolcanoConfig> {
16
+ readonly #scale: number;
17
+ readonly #color: string;
18
+ readonly #smokeColor: string;
19
+ #speed: number;
20
+ #intensity: number;
21
+ #maxProjectiles: number;
22
+ #maxEmbers: number;
23
+ #time: number = 0;
24
+ #particles: VolcanoProjectile[] = [];
25
+ #canvasWidth: number = 800;
26
+ #canvasHeight: number = 600;
27
+
28
+ constructor(config: VolcanoConfig = {}) {
29
+ super();
30
+
31
+ this.#scale = config.scale ?? 1;
32
+ this.#speed = config.speed ?? 1;
33
+ this.#maxProjectiles = config.projectiles ?? 30;
34
+ this.#maxEmbers = config.embers ?? 60;
35
+ this.#intensity = config.intensity ?? 1;
36
+ this.#color = config.color ?? '#ff4400';
37
+ this.#smokeColor = config.smokeColor ?? '#444444';
38
+
39
+ if (innerWidth < 991) {
40
+ this.#maxProjectiles = Math.floor(this.#maxProjectiles / 2);
41
+ this.#maxEmbers = Math.floor(this.#maxEmbers / 2);
42
+ }
43
+ }
44
+
45
+ configure(config: Partial<VolcanoConfig>): void {
46
+ if (config.speed !== undefined) {
47
+ this.#speed = config.speed;
48
+ }
49
+ if (config.intensity !== undefined) {
50
+ this.#intensity = config.intensity;
51
+ }
52
+ }
53
+
54
+ onResize(width: number, height: number): void {
55
+ this.#canvasWidth = width;
56
+ this.#canvasHeight = height;
57
+ }
58
+
59
+ tick(dt: number, width: number, height: number): void {
60
+ this.#canvasWidth = width;
61
+ this.#canvasHeight = height;
62
+ this.#time += 0.02 * dt * this.#speed;
63
+
64
+ const eruptX = width * 0.5;
65
+ const eruptY = height * 0.85;
66
+
67
+ const lavaCount = this.#countByType('lava');
68
+ const emberCount = this.#countByType('ember');
69
+ const smokeCount = this.#countByType('smoke');
70
+
71
+ if (lavaCount < this.#maxProjectiles && MULBERRY.next() < 0.15 * this.#intensity * dt) {
72
+ this.#particles.push(this.#createLava(eruptX, eruptY));
73
+ }
74
+
75
+ if (emberCount < this.#maxEmbers && MULBERRY.next() < 0.4 * this.#intensity * dt) {
76
+ this.#particles.push(this.#createEmber(eruptX, eruptY));
77
+ }
78
+
79
+ if (smokeCount < 40 && MULBERRY.next() < 0.2 * this.#intensity * dt) {
80
+ this.#particles.push(this.#createSmoke(eruptX, eruptY));
81
+ }
82
+
83
+ let alive = 0;
84
+
85
+ for (let i = 0; i < this.#particles.length; i++) {
86
+ const particle = this.#particles[i];
87
+
88
+ particle.x += particle.vx * dt * this.#speed;
89
+ particle.y += particle.vy * dt * this.#speed;
90
+ particle.life += dt;
91
+
92
+ if (particle.type === 'lava') {
93
+ particle.vy += 0.015 * dt;
94
+ particle.vx *= 0.999;
95
+ } else if (particle.type === 'ember') {
96
+ particle.vy -= 0.002 * dt;
97
+ particle.vx += (MULBERRY.next() - 0.5) * 0.02 * dt;
98
+ } else {
99
+ particle.vy -= 0.003 * dt;
100
+ particle.vx += (MULBERRY.next() - 0.5) * 0.01 * dt;
101
+ particle.size += 0.03 * dt;
102
+ }
103
+
104
+ if (particle.life < particle.maxLife) {
105
+ this.#particles[alive++] = particle;
106
+ }
107
+ }
108
+
109
+ this.#particles.length = alive;
110
+ }
111
+
112
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
113
+ this.#drawGlow(ctx, width, height);
114
+ this.#drawSmoke(ctx);
115
+ this.#drawLavaAndEmbers(ctx);
116
+ }
117
+
118
+ #drawGlow(ctx: CanvasRenderingContext2D, width: number, height: number): void {
119
+ const eruptX = width * 0.5;
120
+ const eruptY = height * 0.85;
121
+ const glowRadius = 120 * this.#scale * this.#intensity;
122
+ const flicker = 0.8 + Math.sin(this.#time * 5) * 0.2;
123
+
124
+ const gradient = ctx.createRadialGradient(eruptX, eruptY, 0, eruptX, eruptY, glowRadius);
125
+ gradient.addColorStop(0, `rgba(255, 200, 50, ${0.4 * flicker * this.#intensity})`);
126
+ gradient.addColorStop(0.3, `rgba(255, 100, 20, ${0.2 * flicker * this.#intensity})`);
127
+ gradient.addColorStop(0.6, `rgba(255, 50, 0, ${0.08 * flicker * this.#intensity})`);
128
+ gradient.addColorStop(1, 'rgba(255, 30, 0, 0)');
129
+
130
+ ctx.globalCompositeOperation = 'lighter';
131
+ ctx.fillStyle = gradient;
132
+ ctx.beginPath();
133
+ ctx.arc(eruptX, eruptY, glowRadius, 0, Math.PI * 2);
134
+ ctx.fill();
135
+ ctx.globalCompositeOperation = 'source-over';
136
+ }
137
+
138
+ #drawSmoke(ctx: CanvasRenderingContext2D): void {
139
+ for (const particle of this.#particles) {
140
+ if (particle.type !== 'smoke') {
141
+ continue;
142
+ }
143
+
144
+ const lifeRatio = particle.life / particle.maxLife;
145
+ const alpha = (1 - lifeRatio) * 0.3 * this.#intensity;
146
+ const size = particle.size * this.#scale;
147
+
148
+ if (alpha < 0.01) {
149
+ continue;
150
+ }
151
+
152
+ ctx.globalAlpha = alpha;
153
+ ctx.fillStyle = this.#smokeColor;
154
+ ctx.beginPath();
155
+ ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2);
156
+ ctx.fill();
157
+ }
158
+
159
+ ctx.globalAlpha = 1;
160
+ }
161
+
162
+ #drawLavaAndEmbers(ctx: CanvasRenderingContext2D): void {
163
+ ctx.globalCompositeOperation = 'lighter';
164
+
165
+ for (const particle of this.#particles) {
166
+ if (particle.type === 'smoke') {
167
+ continue;
168
+ }
169
+
170
+ const lifeRatio = particle.life / particle.maxLife;
171
+ const alpha = (1 - lifeRatio) * this.#intensity;
172
+ const size = particle.size * this.#scale;
173
+
174
+ if (alpha < 0.02) {
175
+ continue;
176
+ }
177
+
178
+ if (particle.type === 'lava') {
179
+ const gradient = ctx.createRadialGradient(
180
+ particle.x, particle.y, 0,
181
+ particle.x, particle.y, size * 2
182
+ );
183
+ gradient.addColorStop(0, `rgba(255, 220, 100, ${alpha})`);
184
+ gradient.addColorStop(0.4, `rgba(255, 100, 20, ${alpha * 0.7})`);
185
+ gradient.addColorStop(1, `rgba(200, 30, 0, 0)`);
186
+
187
+ ctx.fillStyle = gradient;
188
+ ctx.beginPath();
189
+ ctx.arc(particle.x, particle.y, size * 2, 0, Math.PI * 2);
190
+ ctx.fill();
191
+
192
+ ctx.fillStyle = `rgba(255, 240, 180, ${alpha * 0.9})`;
193
+ ctx.beginPath();
194
+ ctx.arc(particle.x, particle.y, size * 0.5, 0, Math.PI * 2);
195
+ ctx.fill();
196
+ } else {
197
+ ctx.fillStyle = `rgba(255, ${150 + MULBERRY.next() * 80 | 0}, 30, ${alpha})`;
198
+ ctx.beginPath();
199
+ ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2);
200
+ ctx.fill();
201
+ }
202
+ }
203
+
204
+ ctx.globalCompositeOperation = 'source-over';
205
+ ctx.globalAlpha = 1;
206
+ }
207
+
208
+ #countByType(type: VolcanoProjectile['type']): number {
209
+ let count = 0;
210
+
211
+ for (let i = 0; i < this.#particles.length; i++) {
212
+ if (this.#particles[i].type === type) {
213
+ count++;
214
+ }
215
+ }
216
+
217
+ return count;
218
+ }
219
+
220
+ #createLava(eruptX: number, eruptY: number): VolcanoProjectile {
221
+ const angle = -Math.PI / 2 + (MULBERRY.next() - 0.5) * 0.8;
222
+ const force = (3 + MULBERRY.next() * 4) * this.#intensity;
223
+
224
+ return {
225
+ x: eruptX + (MULBERRY.next() - 0.5) * 20 * this.#scale,
226
+ y: eruptY,
227
+ vx: Math.cos(angle) * force,
228
+ vy: Math.sin(angle) * force,
229
+ size: 3 + MULBERRY.next() * 5,
230
+ life: 0,
231
+ maxLife: 80 + MULBERRY.next() * 60,
232
+ type: 'lava'
233
+ };
234
+ }
235
+
236
+ #createEmber(eruptX: number, eruptY: number): VolcanoProjectile {
237
+ return {
238
+ x: eruptX + (MULBERRY.next() - 0.5) * 30 * this.#scale,
239
+ y: eruptY - MULBERRY.next() * 10,
240
+ vx: (MULBERRY.next() - 0.5) * 1.5,
241
+ vy: -(0.5 + MULBERRY.next() * 1.5),
242
+ size: 1 + MULBERRY.next() * 2,
243
+ life: 0,
244
+ maxLife: 60 + MULBERRY.next() * 80,
245
+ type: 'ember'
246
+ };
247
+ }
248
+
249
+ #createSmoke(eruptX: number, eruptY: number): VolcanoProjectile {
250
+ return {
251
+ x: eruptX + (MULBERRY.next() - 0.5) * 40 * this.#scale,
252
+ y: eruptY - MULBERRY.next() * 20,
253
+ vx: (MULBERRY.next() - 0.5) * 0.5,
254
+ vy: -(0.3 + MULBERRY.next() * 0.5),
255
+ size: 8 + MULBERRY.next() * 12,
256
+ life: 0,
257
+ maxLife: 120 + MULBERRY.next() * 100,
258
+ type: 'smoke'
259
+ };
260
+ }
261
+ }
@@ -0,0 +1,10 @@
1
+ export type VolcanoProjectile = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ size: number;
7
+ life: number;
8
+ maxLife: number;
9
+ type: 'lava' | 'ember' | 'smoke';
10
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,10 @@
1
+ import { Voronoi } from './layer';
2
+ import type { VoronoiConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createVoronoi(config?: VoronoiConfig): Effect<VoronoiConfig> {
6
+ return new Voronoi(config);
7
+ }
8
+
9
+ export type { VoronoiConfig };
10
+ export type { VoronoiCell } from './types';