@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
@@ -1,49 +1,44 @@
1
- import { LimitedFrameRateCanvas } from '../canvas';
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
2
3
  import { MULBERRY } from './consts';
3
4
  import type { Snowflake } from './snowflake';
4
5
 
5
- export interface SnowSimulationConfig {
6
+ export interface SnowConfig {
6
7
  readonly fillStyle?: string;
7
8
  readonly particles?: number;
8
9
  readonly scale?: number;
9
10
  readonly size?: number;
10
11
  readonly speed?: number;
11
- readonly canvasOptions?: CanvasRenderingContext2DSettings;
12
12
  }
13
13
 
14
14
  const SPRITE_SIZE = 64;
15
15
  const SPRITE_CENTER = SPRITE_SIZE / 2;
16
16
  const SPRITE_RADIUS = SPRITE_SIZE / 2;
17
17
 
18
- export class SnowSimulation extends LimitedFrameRateCanvas {
18
+ export class Snow extends Effect<SnowConfig> {
19
19
  readonly #scale: number;
20
20
  readonly #size: number;
21
- readonly #speed: number;
21
+ #speed: number;
22
22
  readonly #baseOpacity: number;
23
23
  #maxParticles: number;
24
24
  #time: number = 0;
25
25
  #ratio: number = 1;
26
26
  #snowflakes: Snowflake[] = [];
27
27
  #sprites: HTMLCanvasElement[] = [];
28
+ #height: number = 540;
28
29
 
29
- constructor(canvas: HTMLCanvasElement, config: SnowSimulationConfig = {}) {
30
- super(canvas, 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
30
+ constructor(config: SnowConfig = {}) {
31
+ super();
31
32
 
32
33
  this.#scale = config.scale ?? 1;
33
34
  this.#maxParticles = config.particles ?? 200;
34
35
  this.#size = (config.size ?? 9) * this.#scale;
35
36
  this.#speed = config.speed ?? 2;
36
37
 
37
- const {r, g, b, a} = this.#parseColor(config.fillStyle ?? 'rgb(255 255 255 / .75)');
38
+ const {r, g, b, a} = parseColor(config.fillStyle ?? 'rgb(255 255 255 / .75)');
38
39
  this.#baseOpacity = a;
39
40
 
40
- this.canvas.style.position = 'absolute';
41
- this.canvas.style.top = '0';
42
- this.canvas.style.left = '0';
43
- this.canvas.style.height = '100%';
44
- this.canvas.style.width = '100%';
45
-
46
- if (this.isSmall) {
41
+ if (innerWidth < 991) {
47
42
  this.#maxParticles = Math.floor(this.#maxParticles / 2);
48
43
  }
49
44
 
@@ -54,77 +49,37 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
54
49
  }
55
50
  }
56
51
 
57
- draw(): void {
58
- this.canvas.height = this.height;
59
- this.canvas.width = this.width;
60
-
61
- const ctx = this.context;
62
- ctx.clearRect(0, 0, this.width, this.height);
63
-
64
- for (const snowflake of this.#snowflakes) {
65
- const px = snowflake.x * this.width;
66
- const py = snowflake.y * this.height;
67
- const displayRadius = snowflake.radius * snowflake.depth * this.#ratio;
68
- const displaySize = displayRadius * 2;
69
-
70
- if (displaySize < 0.5) {
71
- continue;
72
- }
73
-
74
- ctx.globalAlpha = this.#baseOpacity * (0.15 + snowflake.depth * 0.85);
75
-
76
- if (snowflake.spriteIndex === 3) {
77
- ctx.save();
78
- ctx.translate(px, py);
79
- ctx.rotate(snowflake.rotation);
80
- ctx.drawImage(
81
- this.#sprites[snowflake.spriteIndex],
82
- -displayRadius,
83
- -displayRadius,
84
- displaySize,
85
- displaySize
86
- );
87
- ctx.restore();
88
- } else {
89
- ctx.drawImage(
90
- this.#sprites[snowflake.spriteIndex],
91
- px - displayRadius,
92
- py - displayRadius,
93
- displaySize,
94
- displaySize
95
- );
96
- }
52
+ configure(config: Partial<SnowConfig>): void {
53
+ if (config.speed !== undefined) {
54
+ this.#speed = config.speed;
97
55
  }
56
+ }
98
57
 
99
- ctx.globalAlpha = 1;
58
+ onResize(_width: number, height: number): void {
59
+ this.#height = height;
100
60
  }
101
61
 
102
- tick(): void {
103
- const speedFactor = (this.height / (420 * this.#ratio) / this.#speed) * this.deltaFactor;
62
+ tick(dt: number, _width: number, height: number): void {
63
+ this.#height = height;
104
64
 
105
- this.#time += 0.015 * speedFactor;
65
+ const speedFactor = height / (420 * this.#ratio) / this.#speed;
66
+
67
+ this.#time += 0.015 * speedFactor * dt;
106
68
 
107
- // Multi-frequency wind for organic movement
108
69
  const wind = Math.sin(this.#time * 0.7) * 0.5
109
- + Math.sin(this.#time * 1.9 + 3) * 0.25
110
- + Math.sin(this.#time * 4.3 + 1) * 0.1;
70
+ + Math.sin(this.#time * 1.9 + 3) * 0.25
71
+ + Math.sin(this.#time * 4.3 + 1) * 0.1;
111
72
 
112
73
  for (let index = 0; index < this.#snowflakes.length; index++) {
113
74
  const snowflake = this.#snowflakes[index];
114
75
 
115
- // Individual swing oscillation
116
76
  const swing = Math.sin(this.#time * snowflake.swingFrequency + snowflake.swingOffset) * snowflake.swingAmplitude;
117
77
 
118
- // Horizontal: personal swing + global wind (deeper = more wind influence)
119
- snowflake.x += (swing + wind * snowflake.depth * 2) / (4000 * speedFactor);
120
-
121
- // Vertical: individual speed + depth + size influence
122
- snowflake.y += (snowflake.fallSpeed * 2 + snowflake.depth + snowflake.radius * 0.15) / (700 * speedFactor);
78
+ snowflake.x += (swing + wind * snowflake.depth * 2) * dt / (4000 * speedFactor);
79
+ snowflake.y += (snowflake.fallSpeed * 2 + snowflake.depth + snowflake.radius * 0.15) * dt / (700 * speedFactor);
123
80
 
124
- // Rotation (only visually relevant for crystal sprites)
125
- snowflake.rotation += snowflake.rotationSpeed / speedFactor;
81
+ snowflake.rotation += snowflake.rotationSpeed * dt / speedFactor;
126
82
 
127
- // Recycle out-of-bounds particles
128
83
  if (snowflake.x > 1.15 || snowflake.x < -0.15 || snowflake.y > 1.05) {
129
84
  const recycled = this.#createSnowflake(false);
130
85
 
@@ -147,26 +102,52 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
147
102
  }
148
103
  }
149
104
 
150
- #parseColor(fillStyle: string): {r: number; g: number; b: number; a: number} {
151
- const canvas = document.createElement('canvas');
152
- canvas.width = 1;
153
- canvas.height = 1;
154
- const ctx = canvas.getContext('2d')!;
155
- ctx.fillStyle = fillStyle;
156
- ctx.fillRect(0, 0, 1, 1);
157
- const data = ctx.getImageData(0, 0, 1, 1).data;
158
- return {r: data[0], g: data[1], b: data[2], a: data[3] / 255};
105
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
106
+
107
+ for (const snowflake of this.#snowflakes) {
108
+ const px = snowflake.x * width;
109
+ const py = snowflake.y * height;
110
+ const displayRadius = snowflake.radius * snowflake.depth * this.#ratio;
111
+ const displaySize = displayRadius * 2;
112
+
113
+ if (displaySize < 0.5) {
114
+ continue;
115
+ }
116
+
117
+ ctx.globalAlpha = this.#baseOpacity * (0.15 + snowflake.depth * 0.85);
118
+
119
+ if (snowflake.spriteIndex === 3) {
120
+ const cos = Math.cos(snowflake.rotation);
121
+ const sin = Math.sin(snowflake.rotation);
122
+ ctx.setTransform(cos, sin, -sin, cos, px, py);
123
+ ctx.drawImage(
124
+ this.#sprites[snowflake.spriteIndex],
125
+ -displayRadius,
126
+ -displayRadius,
127
+ displaySize,
128
+ displaySize
129
+ );
130
+ ctx.resetTransform();
131
+ } else {
132
+ ctx.drawImage(
133
+ this.#sprites[snowflake.spriteIndex],
134
+ px - displayRadius,
135
+ py - displayRadius,
136
+ displaySize,
137
+ displaySize
138
+ );
139
+ }
140
+ }
141
+
142
+ ctx.globalAlpha = 1;
159
143
  }
160
144
 
161
145
  #createSprites(r: number, g: number, b: number): HTMLCanvasElement[] {
162
146
  const sprites: HTMLCanvasElement[] = [];
163
147
 
164
148
  const gradientProfiles: [number, number][][] = [
165
- // 0: Soft glow
166
149
  [[0, 0.8], [0.3, 0.4], [0.7, 0.1], [1, 0]],
167
- // 1: Bright center
168
150
  [[0, 1], [0.15, 0.7], [0.5, 0.2], [1, 0]],
169
- // 2: Compact dot
170
151
  [[0, 0.9], [0.25, 0.5], [0.5, 0.1], [1, 0]]
171
152
  ];
172
153
 
@@ -193,7 +174,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
193
174
  sprites.push(canvas);
194
175
  }
195
176
 
196
- // 3: Crystal snowflake
197
177
  sprites.push(this.#createCrystalSprite(r, g, b));
198
178
 
199
179
  return sprites;
@@ -205,7 +185,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
205
185
  canvas.height = SPRITE_SIZE;
206
186
  const ctx = canvas.getContext('2d')!;
207
187
 
208
- // Soft glow base
209
188
  const glow = ctx.createRadialGradient(
210
189
  SPRITE_CENTER, SPRITE_CENTER, 0,
211
190
  SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
@@ -219,7 +198,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
219
198
  ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
220
199
  ctx.fill();
221
200
 
222
- // Crystal arms
223
201
  ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.7)`;
224
202
  ctx.lineWidth = 1.5;
225
203
  ctx.lineCap = 'round';
@@ -231,13 +209,11 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
231
209
  const tipX = SPRITE_CENTER + Math.cos(angle) * armLength;
232
210
  const tipY = SPRITE_CENTER + Math.sin(angle) * armLength;
233
211
 
234
- // Main arm
235
212
  ctx.beginPath();
236
213
  ctx.moveTo(SPRITE_CENTER, SPRITE_CENTER);
237
214
  ctx.lineTo(tipX, tipY);
238
215
  ctx.stroke();
239
216
 
240
- // Side branches at 40% and 65% along the arm
241
217
  for (const position of [0.4, 0.65]) {
242
218
  const branchX = SPRITE_CENTER + Math.cos(angle) * armLength * position;
243
219
  const branchY = SPRITE_CENTER + Math.sin(angle) * armLength * position;
@@ -256,7 +232,6 @@ export class SnowSimulation extends LimitedFrameRateCanvas {
256
232
  }
257
233
  }
258
234
 
259
- // Center dot
260
235
  const centerGlow = ctx.createRadialGradient(
261
236
  SPRITE_CENTER, SPRITE_CENTER, 0,
262
237
  SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS * 0.12
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,16 @@
1
+ import { Sparklers } from './layer';
2
+ import type { SparklersConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export interface SparklersInstance extends Effect<SparklersConfig> {
6
+ moveTo(x: number, y: number): void;
7
+ }
8
+
9
+ export function createSparklers(config?: SparklersConfig): SparklersInstance {
10
+ return new Sparklers(config) as SparklersInstance;
11
+ }
12
+
13
+ export { SparklerParticle } from './particle';
14
+ export type { SparklersConfig };
15
+ export type { SparklerParticleConfig } from './particle';
16
+ export type { SparklerSpark } from './types';
@@ -0,0 +1,220 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { SparklerSpark } from './types';
5
+
6
+ export interface SparklersConfig {
7
+ readonly emitRate?: number;
8
+ readonly maxSparks?: number;
9
+ readonly colors?: string[];
10
+ readonly speed?: [number, number];
11
+ readonly friction?: number;
12
+ readonly gravity?: number;
13
+ readonly decay?: [number, number];
14
+ readonly trailLength?: number;
15
+ readonly hoverMode?: boolean;
16
+ readonly scale?: number;
17
+ }
18
+
19
+ const DEFAULT_COLORS = ['#ffcc33', '#ff9900', '#ffffff', '#ffee88'];
20
+
21
+ export class Sparklers extends Effect<SparklersConfig> {
22
+ #scale: number;
23
+ #emitRate: number;
24
+ readonly #maxSparks: number;
25
+ readonly #colorRGBs: [number, number, number][];
26
+ readonly #speedRange: [number, number];
27
+ #friction: number;
28
+ #gravity: number;
29
+ readonly #decayRange: [number, number];
30
+ #trailLength: number;
31
+ #hoverMode: boolean;
32
+ readonly #onMouseMoveBound: (evt: MouseEvent) => void;
33
+ readonly #onMouseLeaveBound: () => void;
34
+ #emitX: number = 0.5;
35
+ #emitY: number = 0.5;
36
+ #mouseOnCanvas: boolean = false;
37
+ #sparks: SparklerSpark[] = [];
38
+ #mountedCanvas: HTMLCanvasElement | null = null;
39
+ #cachedRect: DOMRect | null = null;
40
+
41
+ constructor(config: SparklersConfig = {}) {
42
+ super();
43
+
44
+ this.#scale = config.scale ?? 1;
45
+ this.#emitRate = config.emitRate ?? 8;
46
+ this.#maxSparks = config.maxSparks ?? 300;
47
+ this.#speedRange = config.speed ?? [2, 8];
48
+ this.#friction = config.friction ?? 0.96;
49
+ this.#gravity = config.gravity ?? 0.8;
50
+ this.#decayRange = config.decay ?? [0.02, 0.05];
51
+ this.#trailLength = config.trailLength ?? 3;
52
+ this.#hoverMode = config.hoverMode ?? false;
53
+
54
+ const colors = config.colors ?? DEFAULT_COLORS;
55
+ this.#colorRGBs = colors.map(c => hexToRGB(c));
56
+
57
+ this.#onMouseMoveBound = this.#onMouseMove.bind(this);
58
+ this.#onMouseLeaveBound = this.#onMouseLeave.bind(this);
59
+ }
60
+
61
+ moveTo(x: number, y: number): void {
62
+ this.#emitX = x;
63
+ this.#emitY = y;
64
+ }
65
+
66
+ onMount(canvas: HTMLCanvasElement): void {
67
+ this.#mountedCanvas = canvas;
68
+ this.#cachedRect = canvas.getBoundingClientRect();
69
+
70
+ if (this.#hoverMode) {
71
+ canvas.addEventListener('mousemove', this.#onMouseMoveBound, {passive: true});
72
+ canvas.addEventListener('mouseleave', this.#onMouseLeaveBound, {passive: true});
73
+ }
74
+ }
75
+
76
+ onUnmount(canvas: HTMLCanvasElement): void {
77
+ canvas.removeEventListener('mousemove', this.#onMouseMoveBound);
78
+ canvas.removeEventListener('mouseleave', this.#onMouseLeaveBound);
79
+ this.#mountedCanvas = null;
80
+ this.#cachedRect = null;
81
+ }
82
+
83
+ onResize(): void {
84
+ if (this.#mountedCanvas) {
85
+ this.#cachedRect = this.#mountedCanvas.getBoundingClientRect();
86
+ }
87
+ }
88
+
89
+ configure(config: Partial<SparklersConfig>): void {
90
+ if (config.scale !== undefined) {
91
+ this.#scale = config.scale;
92
+ }
93
+ if (config.emitRate !== undefined) {
94
+ this.#emitRate = config.emitRate;
95
+ }
96
+ if (config.friction !== undefined) {
97
+ this.#friction = config.friction;
98
+ }
99
+ if (config.gravity !== undefined) {
100
+ this.#gravity = config.gravity;
101
+ }
102
+ if (config.trailLength !== undefined) {
103
+ this.#trailLength = config.trailLength;
104
+ }
105
+ if (config.hoverMode !== undefined) {
106
+ this.#hoverMode = config.hoverMode;
107
+ }
108
+ }
109
+
110
+ tick(dt: number, width: number, height: number): void {
111
+ if (!this.#hoverMode || this.#mouseOnCanvas) {
112
+ const emitCount = Math.min(this.#emitRate, this.#maxSparks - this.#sparks.length);
113
+
114
+ for (let i = 0; i < emitCount; i++) {
115
+ this.#sparks.push(this.#createSpark(width, height));
116
+ }
117
+ }
118
+
119
+ const frictionFactor = Math.pow(this.#friction, dt);
120
+ let alive = 0;
121
+
122
+ for (let i = 0; i < this.#sparks.length; i++) {
123
+ const spark = this.#sparks[i];
124
+
125
+ spark.trail.push({x: spark.x, y: spark.y});
126
+
127
+ if (spark.trail.length > this.#trailLength) {
128
+ spark.trail.shift();
129
+ }
130
+
131
+ spark.vx *= frictionFactor;
132
+ spark.vy *= frictionFactor;
133
+ spark.vy += this.#gravity * this.#scale * dt;
134
+
135
+ spark.x += spark.vx * dt;
136
+ spark.y += spark.vy * dt;
137
+
138
+ spark.alpha -= spark.decay * dt;
139
+
140
+ if (spark.alpha > 0) {
141
+ this.#sparks[alive++] = spark;
142
+ }
143
+ }
144
+
145
+ this.#sparks.length = alive;
146
+ }
147
+
148
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
149
+ ctx.globalCompositeOperation = 'lighter';
150
+
151
+ if (!this.#hoverMode || this.#mouseOnCanvas) {
152
+ const cx = this.#emitX * width;
153
+ const cy = this.#emitY * height;
154
+ const glowRadius = 15 * this.#scale;
155
+ const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, glowRadius);
156
+ glow.addColorStop(0, 'rgba(255, 220, 100, 0.8)');
157
+ glow.addColorStop(0.3, 'rgba(255, 180, 50, 0.3)');
158
+ glow.addColorStop(1, 'rgba(255, 150, 0, 0)');
159
+ ctx.fillStyle = glow;
160
+ ctx.beginPath();
161
+ ctx.arc(cx, cy, glowRadius, 0, Math.PI * 2);
162
+ ctx.fill();
163
+ }
164
+
165
+ for (const spark of this.#sparks) {
166
+ const [r, g, b] = spark.color;
167
+
168
+ for (let t = 0; t < spark.trail.length; t++) {
169
+ const trailAlpha = spark.alpha * (t / spark.trail.length) * 0.5;
170
+
171
+ if (trailAlpha < 0.01) {
172
+ continue;
173
+ }
174
+
175
+ const trailSize = spark.size * (t / spark.trail.length) * this.#scale;
176
+
177
+ ctx.beginPath();
178
+ ctx.arc(spark.trail[t].x, spark.trail[t].y, trailSize, 0, Math.PI * 2);
179
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${trailAlpha})`;
180
+ ctx.fill();
181
+ }
182
+
183
+ ctx.beginPath();
184
+ ctx.arc(spark.x, spark.y, spark.size * this.#scale, 0, Math.PI * 2);
185
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${spark.alpha})`;
186
+ ctx.fill();
187
+ }
188
+
189
+ ctx.globalCompositeOperation = 'source-over';
190
+ }
191
+
192
+ #onMouseMove(evt: MouseEvent): void {
193
+ const rect = this.#cachedRect ?? (evt.currentTarget as HTMLCanvasElement).getBoundingClientRect();
194
+ this.#emitX = (evt.clientX - rect.left) / rect.width;
195
+ this.#emitY = (evt.clientY - rect.top) / rect.height;
196
+ this.#mouseOnCanvas = true;
197
+ }
198
+
199
+ #onMouseLeave(): void {
200
+ this.#mouseOnCanvas = false;
201
+ }
202
+
203
+ #createSpark(width: number, height: number): SparklerSpark {
204
+ const angle = MULBERRY.next() * Math.PI * 2;
205
+ const speed = this.#speedRange[0] + MULBERRY.next() * (this.#speedRange[1] - this.#speedRange[0]);
206
+ const colorIndex = Math.floor(MULBERRY.next() * this.#colorRGBs.length);
207
+
208
+ return {
209
+ x: this.#emitX * width,
210
+ y: this.#emitY * height,
211
+ vx: Math.cos(angle) * speed * this.#scale,
212
+ vy: Math.sin(angle) * speed * this.#scale,
213
+ alpha: 0.8 + MULBERRY.next() * 0.2,
214
+ color: this.#colorRGBs[colorIndex],
215
+ size: 1 + MULBERRY.next() * 2,
216
+ decay: this.#decayRange[0] + MULBERRY.next() * (this.#decayRange[1] - this.#decayRange[0]),
217
+ trail: []
218
+ };
219
+ }
220
+ }
@@ -0,0 +1,89 @@
1
+ import type { Point } from '../point';
2
+
3
+ export interface SparklerParticleConfig {
4
+ readonly decay?: number;
5
+ readonly friction?: number;
6
+ readonly gravity?: number;
7
+ readonly scale?: number;
8
+ readonly size?: number;
9
+ readonly trailLength?: number;
10
+ }
11
+
12
+ export class SparklerParticle {
13
+ readonly #color: [number, number, number];
14
+ readonly #decay: number;
15
+ readonly #friction: number;
16
+ readonly #gravity: number;
17
+ readonly #scale: number;
18
+ readonly #size: number;
19
+ readonly #trailLength: number;
20
+ readonly #trail: Point[] = [];
21
+ #x: number;
22
+ #y: number;
23
+ #vx: number;
24
+ #vy: number;
25
+ #alpha: number = 1;
26
+
27
+ get isDead(): boolean {
28
+ return this.#alpha <= 0;
29
+ }
30
+
31
+ get position(): Point {
32
+ return {x: this.#x, y: this.#y};
33
+ }
34
+
35
+ constructor(position: Point, velocity: Point, color: [number, number, number], config: SparklerParticleConfig = {}) {
36
+ this.#x = position.x;
37
+ this.#y = position.y;
38
+ this.#vx = velocity.x;
39
+ this.#vy = velocity.y;
40
+ this.#color = color;
41
+ this.#decay = config.decay ?? (0.02 + Math.random() * 0.03);
42
+ this.#friction = config.friction ?? 0.96;
43
+ this.#gravity = config.gravity ?? 0.8;
44
+ this.#scale = config.scale ?? 1;
45
+ this.#size = config.size ?? (1 + Math.random() * 2);
46
+ this.#trailLength = config.trailLength ?? 3;
47
+ }
48
+
49
+ draw(ctx: CanvasRenderingContext2D): void {
50
+ const [r, g, b] = this.#color;
51
+
52
+ for (let t = 0; t < this.#trail.length; t++) {
53
+ const trailAlpha = this.#alpha * (t / this.#trail.length) * 0.5;
54
+
55
+ if (trailAlpha < 0.01) {
56
+ continue;
57
+ }
58
+
59
+ const trailSize = this.#size * (t / this.#trail.length) * this.#scale;
60
+
61
+ ctx.beginPath();
62
+ ctx.arc(this.#trail[t].x, this.#trail[t].y, trailSize, 0, Math.PI * 2);
63
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${trailAlpha})`;
64
+ ctx.fill();
65
+ }
66
+
67
+ ctx.beginPath();
68
+ ctx.arc(this.#x, this.#y, this.#size * this.#scale, 0, Math.PI * 2);
69
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${this.#alpha})`;
70
+ ctx.fill();
71
+ }
72
+
73
+ tick(dt: number = 1): void {
74
+ this.#trail.push({x: this.#x, y: this.#y});
75
+
76
+ if (this.#trail.length > this.#trailLength) {
77
+ this.#trail.shift();
78
+ }
79
+
80
+ this.#vx *= Math.pow(this.#friction, dt);
81
+ this.#vy *= Math.pow(this.#friction, dt);
82
+ this.#vy += this.#gravity * this.#scale * dt;
83
+
84
+ this.#x += this.#vx * dt;
85
+ this.#y += this.#vy * dt;
86
+
87
+ this.#alpha -= this.#decay * dt;
88
+ }
89
+ }
@@ -0,0 +1,13 @@
1
+ import type { Point } from '../point';
2
+
3
+ export type SparklerSpark = {
4
+ x: number;
5
+ y: number;
6
+ vx: number;
7
+ vy: number;
8
+ alpha: number;
9
+ color: [number, number, number];
10
+ size: number;
11
+ decay: number;
12
+ trail: Point[];
13
+ };
@@ -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 { Stars } from './layer';
2
+ import type { StarsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createStars(config?: StarsConfig): Effect<StarsConfig> {
6
+ return new Stars(config);
7
+ }
8
+
9
+ export type { StarsConfig };
10
+ export type { Star, StarMode, ShootingStar } from './types';