@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,360 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { NetworkParticle, ParticleMouseMode } from './types';
5
+
6
+ export interface ParticlesConfig {
7
+ readonly count?: number;
8
+ readonly color?: string;
9
+ readonly lineColor?: string;
10
+ readonly size?: [number, number];
11
+ readonly speed?: [number, number];
12
+ readonly connectionDistance?: number;
13
+ readonly lineWidth?: number;
14
+ readonly mouseMode?: ParticleMouseMode;
15
+ readonly mouseRadius?: number;
16
+ readonly mouseStrength?: number;
17
+ readonly particleForces?: boolean;
18
+ readonly glow?: boolean;
19
+ readonly background?: string | null;
20
+ readonly scale?: number;
21
+ }
22
+
23
+ export class Particles extends Effect<ParticlesConfig> {
24
+ #scale: number;
25
+ #connectionDistance: number;
26
+ #lineWidth: number;
27
+ #mouseMode: ParticleMouseMode;
28
+ #mouseRadius: number;
29
+ #mouseStrength: number;
30
+ #particleForces: boolean;
31
+ #glow: boolean;
32
+ readonly #background: string | null;
33
+ readonly #colorRGB: [number, number, number];
34
+ readonly #lineColorRGB: [number, number, number];
35
+ readonly #sizeRange: [number, number];
36
+ readonly #speedRange: [number, number];
37
+ readonly #onMouseMoveBound: (evt: MouseEvent) => void;
38
+ readonly #onMouseLeaveBound: () => void;
39
+ #maxCount: number;
40
+ #mouseX: number = -1;
41
+ #mouseY: number = -1;
42
+ #mouseOnCanvas: boolean = false;
43
+ #particles: NetworkParticle[] = [];
44
+ #grid: Map<string, number[]> = new Map();
45
+ #cellSize: number;
46
+ #width: number = 960;
47
+ #height: number = 540;
48
+ #initialized: boolean = false;
49
+
50
+ constructor(config: ParticlesConfig = {}) {
51
+ super();
52
+
53
+ this.#scale = config.scale ?? 1;
54
+ this.#maxCount = config.count ?? 100;
55
+ this.#connectionDistance = (config.connectionDistance ?? 120) * this.#scale;
56
+ this.#lineWidth = config.lineWidth ?? 0.5;
57
+ this.#mouseMode = config.mouseMode ?? 'connect';
58
+ this.#mouseRadius = (config.mouseRadius ?? 150) * this.#scale;
59
+ this.#mouseStrength = config.mouseStrength ?? 0.03;
60
+ this.#particleForces = config.particleForces ?? false;
61
+ this.#glow = config.glow ?? false;
62
+ this.#background = config.background ?? null;
63
+ this.#cellSize = this.#connectionDistance;
64
+
65
+ this.#colorRGB = hexToRGB(config.color ?? '#6366f1');
66
+ this.#lineColorRGB = hexToRGB(config.lineColor ?? '#6366f1');
67
+
68
+ this.#sizeRange = config.size ?? [1, 3];
69
+ this.#speedRange = config.speed ?? [0.2, 0.8];
70
+
71
+ if (innerWidth < 991) {
72
+ this.#maxCount = Math.floor(this.#maxCount / 2);
73
+ }
74
+
75
+ this.#onMouseMoveBound = this.#onMouseMove.bind(this);
76
+ this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
77
+ }
78
+
79
+ configure(config: Partial<ParticlesConfig>): void {
80
+ if (config.scale !== undefined) {
81
+ this.#scale = config.scale;
82
+ }
83
+ if (config.connectionDistance !== undefined) {
84
+ this.#connectionDistance = config.connectionDistance * this.#scale;
85
+ }
86
+ if (config.lineWidth !== undefined) {
87
+ this.#lineWidth = config.lineWidth;
88
+ }
89
+ if (config.mouseMode !== undefined) {
90
+ this.#mouseMode = config.mouseMode;
91
+ }
92
+ if (config.mouseRadius !== undefined) {
93
+ this.#mouseRadius = config.mouseRadius * this.#scale;
94
+ }
95
+ if (config.mouseStrength !== undefined) {
96
+ this.#mouseStrength = config.mouseStrength;
97
+ }
98
+ if (config.particleForces !== undefined) {
99
+ this.#particleForces = config.particleForces;
100
+ }
101
+ if (config.glow !== undefined) {
102
+ this.#glow = config.glow;
103
+ }
104
+ }
105
+
106
+ onResize(width: number, height: number): void {
107
+ this.#width = width;
108
+ this.#height = height;
109
+
110
+ if (!this.#initialized) {
111
+ this.#initialized = true;
112
+ this.#particles = [];
113
+
114
+ for (let i = 0; i < this.#maxCount; ++i) {
115
+ this.#particles.push(this.#createParticle(this.#sizeRange, this.#speedRange));
116
+ }
117
+ }
118
+ }
119
+
120
+ onMount(canvas: HTMLCanvasElement): void {
121
+ if (this.#mouseMode !== 'none') {
122
+ canvas.addEventListener('mousemove', this.#onMouseMoveBound, {passive: true});
123
+ canvas.addEventListener('mouseleave', this.#onMouseLeaveBound, {passive: true});
124
+ }
125
+ }
126
+
127
+ onUnmount(canvas: HTMLCanvasElement): void {
128
+ canvas.removeEventListener('mousemove', this.#onMouseMoveBound);
129
+ canvas.removeEventListener('mouseleave', this.#onMouseLeaveBound);
130
+ }
131
+
132
+ tick(dt: number, width: number, height: number): void {
133
+ this.#width = width;
134
+ this.#height = height;
135
+
136
+ this.#grid.clear();
137
+
138
+ for (let i = 0; i < this.#particles.length; i++) {
139
+ const particle = this.#particles[i];
140
+ const col = Math.floor(particle.x / this.#cellSize);
141
+ const row = Math.floor(particle.y / this.#cellSize);
142
+ const key = `${col},${row}`;
143
+ const cell = this.#grid.get(key);
144
+
145
+ if (cell) {
146
+ cell.push(i);
147
+ } else {
148
+ this.#grid.set(key, [i]);
149
+ }
150
+ }
151
+
152
+ for (const particle of this.#particles) {
153
+ particle.x += particle.vx * dt;
154
+ particle.y += particle.vy * dt;
155
+
156
+ if (particle.x < 0) {
157
+ particle.x = 0;
158
+ particle.vx = Math.abs(particle.vx);
159
+ } else if (particle.x > width) {
160
+ particle.x = width;
161
+ particle.vx = -Math.abs(particle.vx);
162
+ }
163
+
164
+ if (particle.y < 0) {
165
+ particle.y = 0;
166
+ particle.vy = Math.abs(particle.vy);
167
+ } else if (particle.y > height) {
168
+ particle.y = height;
169
+ particle.vy = -Math.abs(particle.vy);
170
+ }
171
+ }
172
+
173
+ if (this.#mouseOnCanvas && (this.#mouseMode === 'attract' || this.#mouseMode === 'repel')) {
174
+ const direction = this.#mouseMode === 'attract' ? -1 : 1;
175
+
176
+ for (const particle of this.#particles) {
177
+ const dx = particle.x - this.#mouseX;
178
+ const dy = particle.y - this.#mouseY;
179
+ const dist = Math.sqrt(dx * dx + dy * dy);
180
+
181
+ if (dist < this.#mouseRadius && dist > 0) {
182
+ const force = (1 - dist / this.#mouseRadius) * this.#mouseStrength * dt;
183
+ particle.vx += (dx / dist) * force * direction * 100;
184
+ particle.vy += (dy / dist) * force * direction * 100;
185
+ }
186
+ }
187
+ }
188
+
189
+ if (this.#particleForces) {
190
+ const repelDist = 30 * this.#scale;
191
+
192
+ for (let i = 0; i < this.#particles.length; i++) {
193
+ const pa = this.#particles[i];
194
+ const col = Math.floor(pa.x / this.#cellSize);
195
+ const row = Math.floor(pa.y / this.#cellSize);
196
+
197
+ for (let dc = -1; dc <= 1; dc++) {
198
+ for (let dr = -1; dr <= 1; dr++) {
199
+ const key = `${col + dc},${row + dr}`;
200
+ const neighbors = this.#grid.get(key);
201
+
202
+ if (!neighbors) {
203
+ continue;
204
+ }
205
+
206
+ for (const j of neighbors) {
207
+ if (j <= i) {
208
+ continue;
209
+ }
210
+
211
+ const pb = this.#particles[j];
212
+ const dx = pa.x - pb.x;
213
+ const dy = pa.y - pb.y;
214
+ const dist = Math.sqrt(dx * dx + dy * dy);
215
+
216
+ if (dist < repelDist && dist > 0) {
217
+ const force = (1 - dist / repelDist) * 0.5 * dt;
218
+ const nx = dx / dist;
219
+ const ny = dy / dist;
220
+ pa.vx += nx * force;
221
+ pa.vy += ny * force;
222
+ pb.vx -= nx * force;
223
+ pb.vy -= ny * force;
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ for (const particle of this.#particles) {
232
+ const currentSpeed = Math.sqrt(particle.vx * particle.vx + particle.vy * particle.vy);
233
+
234
+ if (currentSpeed > particle.baseSpeed) {
235
+ const damping = Math.pow(0.98, dt);
236
+ particle.vx *= damping;
237
+ particle.vy *= damping;
238
+ }
239
+ }
240
+ }
241
+
242
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
243
+ if (this.#background) {
244
+ ctx.fillStyle = this.#background;
245
+ ctx.fillRect(0, 0, width, height);
246
+ }
247
+
248
+ const [lr, lg, lb] = this.#lineColorRGB;
249
+ const [pr, pg, pb] = this.#colorRGB;
250
+ const connDist = this.#connectionDistance;
251
+
252
+ ctx.lineWidth = this.#lineWidth;
253
+
254
+ const drawn = new Set<string>();
255
+
256
+ for (let i = 0; i < this.#particles.length; i++) {
257
+ const pa = this.#particles[i];
258
+ const col = Math.floor(pa.x / this.#cellSize);
259
+ const row = Math.floor(pa.y / this.#cellSize);
260
+
261
+ for (let dc = -1; dc <= 1; dc++) {
262
+ for (let dr = -1; dr <= 1; dr++) {
263
+ const key = `${col + dc},${row + dr}`;
264
+ const neighbors = this.#grid.get(key);
265
+
266
+ if (!neighbors) {
267
+ continue;
268
+ }
269
+
270
+ for (const j of neighbors) {
271
+ if (j <= i) {
272
+ continue;
273
+ }
274
+
275
+ const pairKey = `${i},${j}`;
276
+
277
+ if (drawn.has(pairKey)) {
278
+ continue;
279
+ }
280
+
281
+ drawn.add(pairKey);
282
+
283
+ const pb2 = this.#particles[j];
284
+ const dx = pa.x - pb2.x;
285
+ const dy = pa.y - pb2.y;
286
+ const dist = Math.sqrt(dx * dx + dy * dy);
287
+
288
+ if (dist < connDist) {
289
+ const alpha = 1 - dist / connDist;
290
+ ctx.beginPath();
291
+ ctx.moveTo(pa.x, pa.y);
292
+ ctx.lineTo(pb2.x, pb2.y);
293
+ ctx.strokeStyle = `rgba(${lr}, ${lg}, ${lb}, ${alpha * 0.6})`;
294
+ ctx.stroke();
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ if (this.#mouseOnCanvas && this.#mouseMode === 'connect') {
302
+ for (const particle of this.#particles) {
303
+ const dx = particle.x - this.#mouseX;
304
+ const dy = particle.y - this.#mouseY;
305
+ const dist = Math.sqrt(dx * dx + dy * dy);
306
+
307
+ if (dist < this.#mouseRadius) {
308
+ const alpha = 1 - dist / this.#mouseRadius;
309
+ ctx.beginPath();
310
+ ctx.moveTo(this.#mouseX, this.#mouseY);
311
+ ctx.lineTo(particle.x, particle.y);
312
+ ctx.strokeStyle = `rgba(${lr}, ${lg}, ${lb}, ${alpha * 0.8})`;
313
+ ctx.stroke();
314
+ }
315
+ }
316
+ }
317
+
318
+ if (this.#glow) {
319
+ ctx.shadowColor = `rgb(${pr}, ${pg}, ${pb})`;
320
+ ctx.shadowBlur = 8 * this.#scale;
321
+ }
322
+
323
+ for (const particle of this.#particles) {
324
+ ctx.beginPath();
325
+ ctx.arc(particle.x, particle.y, particle.radius * this.#scale, 0, Math.PI * 2);
326
+ ctx.fillStyle = `rgb(${pr}, ${pg}, ${pb})`;
327
+ ctx.fill();
328
+ }
329
+
330
+ if (this.#glow) {
331
+ ctx.shadowBlur = 0;
332
+ }
333
+ }
334
+
335
+ #onMouseMove(evt: MouseEvent): void {
336
+ const target = evt.currentTarget as HTMLCanvasElement;
337
+ const rect = target.getBoundingClientRect();
338
+ this.#mouseX = evt.clientX - rect.left;
339
+ this.#mouseY = evt.clientY - rect.top;
340
+ this.#mouseOnCanvas = true;
341
+ }
342
+
343
+ #onMouseLeave(): void {
344
+ this.#mouseOnCanvas = false;
345
+ }
346
+
347
+ #createParticle(sizeRange: [number, number], speedRange: [number, number]): NetworkParticle {
348
+ const angle = MULBERRY.next() * Math.PI * 2;
349
+ const speed = speedRange[0] + MULBERRY.next() * (speedRange[1] - speedRange[0]);
350
+
351
+ return {
352
+ x: MULBERRY.next() * this.#width,
353
+ y: MULBERRY.next() * this.#height,
354
+ vx: Math.cos(angle) * speed,
355
+ vy: Math.sin(angle) * speed,
356
+ radius: sizeRange[0] + MULBERRY.next() * (sizeRange[1] - sizeRange[0]),
357
+ baseSpeed: speed
358
+ };
359
+ }
360
+ }
@@ -0,0 +1,10 @@
1
+ export type ParticleMouseMode = 'attract' | 'repel' | 'connect' | 'none';
2
+
3
+ export type NetworkParticle = {
4
+ x: number;
5
+ y: number;
6
+ vx: number;
7
+ vy: number;
8
+ radius: number;
9
+ baseSpeed: number;
10
+ };
@@ -0,0 +1,13 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const PETAL_COLORS: string[] = [
6
+ '#ffb7c5', // sakura pink
7
+ '#ffc0cb', // pink
8
+ '#ffd1dc', // pastel pink
9
+ '#ffe4e9', // light pink
10
+ '#fff0f3', // near white pink
11
+ '#f8c3cd', // rose pink
12
+ '#f4a7b9' // deeper pink
13
+ ];
@@ -0,0 +1,10 @@
1
+ import { Petals } from './layer';
2
+ import type { PetalsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createPetals(config?: PetalsConfig): Effect<PetalsConfig> {
6
+ return new Petals(config);
7
+ }
8
+
9
+ export type { PetalsConfig };
10
+ export type { Petal } from './types';
@@ -0,0 +1,174 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY, PETAL_COLORS } from './consts';
3
+ import type { Petal } from './types';
4
+
5
+ export interface PetalsConfig {
6
+ readonly count?: number;
7
+ readonly colors?: string[];
8
+ readonly size?: number;
9
+ readonly speed?: number;
10
+ readonly wind?: number;
11
+ readonly scale?: number;
12
+ }
13
+
14
+ export class Petals extends Effect<PetalsConfig> {
15
+ readonly #scale: number;
16
+ readonly #size: number;
17
+ #speed: number;
18
+ #wind: number;
19
+ readonly #colors: string[];
20
+ #maxCount: number;
21
+ #time: number = 0;
22
+ #petals: Petal[] = [];
23
+ #sprites: HTMLCanvasElement[] = [];
24
+
25
+ constructor(config: PetalsConfig = {}) {
26
+ super();
27
+
28
+ this.#scale = config.scale ?? 1;
29
+ this.#maxCount = config.count ?? 100;
30
+ this.#size = (config.size ?? 24) * this.#scale;
31
+ this.#speed = config.speed ?? 0.7;
32
+ this.#wind = config.wind ?? 0.15;
33
+ this.#colors = config.colors ?? PETAL_COLORS;
34
+
35
+ if (innerWidth < 991) {
36
+ this.#maxCount = Math.floor(this.#maxCount / 2);
37
+ }
38
+
39
+ this.#sprites = this.#createSprites();
40
+
41
+ for (let i = 0; i < this.#maxCount; ++i) {
42
+ this.#petals.push(this.#createPetal(true));
43
+ }
44
+ }
45
+
46
+ configure(config: Partial<PetalsConfig>): void {
47
+ if (config.speed !== undefined) {
48
+ this.#speed = config.speed;
49
+ }
50
+ if (config.wind !== undefined) {
51
+ this.#wind = config.wind;
52
+ }
53
+ }
54
+
55
+ tick(dt: number, _width: number, height: number): void {
56
+ const speedFactor = (height / 540) / this.#speed;
57
+
58
+ this.#time += 0.012 * dt;
59
+
60
+ const globalWind = Math.sin(this.#time * 0.4) * 0.3
61
+ + Math.sin(this.#time * 1.1 + 1.5) * 0.15
62
+ + Math.sin(this.#time * 2.7) * 0.08;
63
+
64
+ for (let index = 0; index < this.#petals.length; index++) {
65
+ const petal = this.#petals[index];
66
+
67
+ const swing = Math.sin(this.#time * petal.swingFrequency + petal.swingOffset) * petal.swingAmplitude;
68
+
69
+ petal.x += (swing + (this.#wind + globalWind * 0.4) * petal.depth) * dt / (3000 * speedFactor);
70
+ petal.y += (petal.fallSpeed * 1.5 + petal.depth * 0.5) * dt / (600 * speedFactor);
71
+
72
+ petal.rotation += petal.rotationSpeed * dt;
73
+ petal.flipAngle += petal.flipSpeed * dt;
74
+
75
+ if (petal.x > 1.15 || petal.x < -0.15 || petal.y > 1.05) {
76
+ const recycled = this.#createPetal(false);
77
+
78
+ if (this.#wind + globalWind > 0.1) {
79
+ recycled.x = -0.15;
80
+ recycled.y = MULBERRY.next() * 0.6;
81
+ } else {
82
+ recycled.x = MULBERRY.next();
83
+ recycled.y = -0.05 - MULBERRY.next() * 0.15;
84
+ }
85
+
86
+ this.#petals[index] = recycled;
87
+ }
88
+ }
89
+ }
90
+
91
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
92
+
93
+ for (const petal of this.#petals) {
94
+ const px = petal.x * width;
95
+ const py = petal.y * height;
96
+ const displaySize = petal.size * petal.depth;
97
+ const scaleX = Math.cos(petal.flipAngle);
98
+ const cos = Math.cos(petal.rotation);
99
+ const sin = Math.sin(petal.rotation);
100
+
101
+ ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
102
+ ctx.globalAlpha = 0.4 + petal.depth * 0.6;
103
+ ctx.drawImage(
104
+ this.#sprites[petal.colorIndex % this.#sprites.length],
105
+ -displaySize / 2,
106
+ -displaySize / 2,
107
+ displaySize,
108
+ displaySize
109
+ );
110
+ }
111
+
112
+ ctx.resetTransform();
113
+ ctx.globalAlpha = 1;
114
+ }
115
+
116
+ #createSprites(): HTMLCanvasElement[] {
117
+ const sprites: HTMLCanvasElement[] = [];
118
+
119
+ for (const color of this.#colors) {
120
+ sprites.push(this.#createPetalSprite(color));
121
+ }
122
+
123
+ return sprites;
124
+ }
125
+
126
+ #createPetalSprite(color: string): HTMLCanvasElement {
127
+ const size = 64;
128
+ const canvas = document.createElement('canvas');
129
+ canvas.width = size;
130
+ canvas.height = size;
131
+ const ctx = canvas.getContext('2d')!;
132
+
133
+ const cx = size / 2;
134
+ const cy = size / 2;
135
+ const hw = size * 0.28;
136
+ const hh = size * 0.38;
137
+
138
+ ctx.fillStyle = color;
139
+ ctx.beginPath();
140
+ ctx.moveTo(cx, cy - hh);
141
+ ctx.bezierCurveTo(cx + hw * 1.4, cy - hh * 0.6, cx + hw * 1.1, cy + hh * 0.3, cx, cy + hh);
142
+ ctx.bezierCurveTo(cx - hw * 1.1, cy + hh * 0.3, cx - hw * 1.4, cy - hh * 0.6, cx, cy - hh);
143
+ ctx.fill();
144
+
145
+ ctx.strokeStyle = 'rgba(200, 100, 120, 0.15)';
146
+ ctx.lineWidth = 0.6;
147
+ ctx.beginPath();
148
+ ctx.moveTo(cx, cy - hh * 0.6);
149
+ ctx.quadraticCurveTo(cx + 1, cy, cx, cy + hh * 0.7);
150
+ ctx.stroke();
151
+
152
+ return canvas;
153
+ }
154
+
155
+ #createPetal(initialSpread: boolean): Petal {
156
+ const depth = 0.5 + MULBERRY.next() * 0.5;
157
+
158
+ return {
159
+ x: MULBERRY.next(),
160
+ y: initialSpread ? MULBERRY.next() * 2 - 1 : -0.05 - MULBERRY.next() * 0.15,
161
+ size: (MULBERRY.next() * 0.4 + 0.6) * this.#size,
162
+ depth,
163
+ rotation: MULBERRY.next() * Math.PI * 2,
164
+ rotationSpeed: (MULBERRY.next() - 0.5) * 0.03,
165
+ flipAngle: MULBERRY.next() * Math.PI * 2,
166
+ flipSpeed: 0.015 + MULBERRY.next() * 0.03,
167
+ swingAmplitude: 0.5 + MULBERRY.next() * 1.0,
168
+ swingFrequency: 0.4 + MULBERRY.next() * 1.2,
169
+ swingOffset: MULBERRY.next() * Math.PI * 2,
170
+ fallSpeed: 0.2 + MULBERRY.next() * 0.5,
171
+ colorIndex: Math.floor(MULBERRY.next() * this.#colors.length)
172
+ };
173
+ }
174
+ }
@@ -0,0 +1,15 @@
1
+ export type Petal = {
2
+ x: number;
3
+ y: number;
4
+ size: number;
5
+ depth: number;
6
+ rotation: number;
7
+ rotationSpeed: number;
8
+ flipAngle: number;
9
+ flipSpeed: number;
10
+ swingAmplitude: number;
11
+ swingFrequency: number;
12
+ swingOffset: number;
13
+ fallSpeed: number;
14
+ colorIndex: number;
15
+ };
@@ -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 { Plasma } from './layer';
2
+ import type { PlasmaConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createPlasma(config?: PlasmaConfig): Effect<PlasmaConfig> {
6
+ return new Plasma(config);
7
+ }
8
+
9
+ export type { PlasmaConfig };
10
+ export type { PlasmaColor } from './types';