@basmilius/sparkle 1.0.0 → 2.1.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 (148) hide show
  1. package/dist/index.d.mts +1192 -14
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +4552 -370
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +3 -2
  6. package/src/aurora/consts.ts +3 -0
  7. package/src/aurora/index.ts +4 -0
  8. package/src/aurora/layer.ts +152 -0
  9. package/src/aurora/simulation.ts +19 -0
  10. package/src/aurora/types.ts +13 -0
  11. package/src/balloons/consts.ts +3 -0
  12. package/src/balloons/index.ts +6 -0
  13. package/src/balloons/layer.ts +138 -0
  14. package/src/balloons/particle.ts +110 -0
  15. package/src/balloons/simulation.ts +19 -0
  16. package/src/balloons/types.ts +14 -0
  17. package/src/bubbles/consts.ts +3 -0
  18. package/src/bubbles/index.ts +4 -0
  19. package/src/bubbles/layer.ts +233 -0
  20. package/src/bubbles/simulation.ts +20 -0
  21. package/src/bubbles/types.ts +21 -0
  22. package/src/canvas.ts +20 -1
  23. package/src/color.ts +10 -0
  24. package/src/confetti/consts.ts +13 -13
  25. package/src/confetti/index.ts +6 -0
  26. package/src/confetti/layer.ts +152 -0
  27. package/src/confetti/particle.ts +105 -0
  28. package/src/confetti/shapes.ts +104 -0
  29. package/src/confetti/simulation.ts +9 -203
  30. package/src/confetti/types.ts +4 -1
  31. package/src/distance.ts +1 -1
  32. package/src/donuts/consts.ts +19 -0
  33. package/src/donuts/donut.ts +12 -0
  34. package/src/donuts/index.ts +3 -0
  35. package/src/donuts/layer.ts +270 -0
  36. package/src/donuts/simulation.ts +25 -0
  37. package/src/fireflies/consts.ts +3 -0
  38. package/src/fireflies/index.ts +6 -0
  39. package/src/fireflies/layer.ts +152 -0
  40. package/src/fireflies/particle.ts +124 -0
  41. package/src/fireflies/simulation.ts +18 -0
  42. package/src/fireflies/types.ts +17 -0
  43. package/src/firepit/consts.ts +3 -0
  44. package/src/firepit/index.ts +4 -0
  45. package/src/firepit/layer.ts +174 -0
  46. package/src/firepit/simulation.ts +17 -0
  47. package/src/firepit/types.ts +20 -0
  48. package/src/fireworks/explosion.ts +8 -8
  49. package/src/fireworks/firework.ts +9 -8
  50. package/src/fireworks/index.ts +6 -2
  51. package/src/fireworks/layer.ts +452 -0
  52. package/src/fireworks/simulation.ts +9 -484
  53. package/src/fireworks/spark.ts +7 -7
  54. package/src/glitter/consts.ts +13 -0
  55. package/src/glitter/index.ts +4 -0
  56. package/src/glitter/layer.ts +173 -0
  57. package/src/glitter/simulation.ts +19 -0
  58. package/src/glitter/types.ts +23 -0
  59. package/src/index.ts +28 -0
  60. package/src/lanterns/consts.ts +13 -0
  61. package/src/lanterns/index.ts +4 -0
  62. package/src/lanterns/layer.ts +166 -0
  63. package/src/lanterns/simulation.ts +17 -0
  64. package/src/lanterns/types.ts +14 -0
  65. package/src/layer.ts +24 -0
  66. package/src/layered.ts +185 -0
  67. package/src/leaves/consts.ts +16 -0
  68. package/src/leaves/index.ts +4 -0
  69. package/src/leaves/layer.ts +251 -0
  70. package/src/leaves/simulation.ts +18 -0
  71. package/src/leaves/types.ts +16 -0
  72. package/src/lightning/consts.ts +3 -0
  73. package/src/lightning/index.ts +6 -0
  74. package/src/lightning/layer.ts +41 -0
  75. package/src/lightning/simulation.ts +17 -0
  76. package/src/lightning/system.ts +196 -0
  77. package/src/lightning/types.ts +12 -0
  78. package/src/matrix/consts.ts +5 -0
  79. package/src/matrix/index.ts +4 -0
  80. package/src/matrix/layer.ts +146 -0
  81. package/src/matrix/simulation.ts +18 -0
  82. package/src/matrix/types.ts +8 -0
  83. package/src/orbits/consts.ts +13 -0
  84. package/src/orbits/index.ts +4 -0
  85. package/src/orbits/layer.ts +183 -0
  86. package/src/orbits/simulation.ts +19 -0
  87. package/src/orbits/types.ts +16 -0
  88. package/src/particles/consts.ts +3 -0
  89. package/src/particles/index.ts +4 -0
  90. package/src/particles/layer.ts +317 -0
  91. package/src/particles/simulation.ts +26 -0
  92. package/src/particles/types.ts +10 -0
  93. package/src/petals/consts.ts +13 -0
  94. package/src/petals/index.ts +4 -0
  95. package/src/petals/layer.ts +158 -0
  96. package/src/petals/simulation.ts +18 -0
  97. package/src/petals/types.ts +15 -0
  98. package/src/plasma/consts.ts +3 -0
  99. package/src/plasma/index.ts +4 -0
  100. package/src/plasma/layer.ts +92 -0
  101. package/src/plasma/simulation.ts +17 -0
  102. package/src/plasma/types.ts +5 -0
  103. package/src/rain/consts.ts +3 -0
  104. package/src/rain/index.ts +6 -0
  105. package/src/rain/layer.ts +172 -0
  106. package/src/rain/particle.ts +132 -0
  107. package/src/rain/simulation.ts +21 -0
  108. package/src/rain/types.ts +22 -0
  109. package/src/sandstorm/consts.ts +3 -0
  110. package/src/sandstorm/index.ts +4 -0
  111. package/src/sandstorm/layer.ts +135 -0
  112. package/src/sandstorm/simulation.ts +18 -0
  113. package/src/sandstorm/types.ts +10 -0
  114. package/src/shooting-stars/index.ts +3 -0
  115. package/src/shooting-stars/system.ts +149 -0
  116. package/src/shooting-stars/types.ts +10 -0
  117. package/src/simulation-canvas.ts +47 -0
  118. package/src/snow/consts.ts +2 -2
  119. package/src/snow/index.ts +1 -0
  120. package/src/snow/layer.ts +263 -0
  121. package/src/snow/simulation.ts +4 -288
  122. package/src/sparklers/consts.ts +3 -0
  123. package/src/sparklers/index.ts +6 -0
  124. package/src/sparklers/layer.ts +174 -0
  125. package/src/sparklers/particle.ts +89 -0
  126. package/src/sparklers/simulation.ts +30 -0
  127. package/src/sparklers/types.ts +13 -0
  128. package/src/stars/consts.ts +3 -0
  129. package/src/stars/index.ts +4 -0
  130. package/src/stars/layer.ts +133 -0
  131. package/src/stars/simulation.ts +22 -0
  132. package/src/stars/types.ts +12 -0
  133. package/src/streamers/consts.ts +14 -0
  134. package/src/streamers/index.ts +4 -0
  135. package/src/streamers/layer.ts +211 -0
  136. package/src/streamers/simulation.ts +16 -0
  137. package/src/streamers/types.ts +14 -0
  138. package/src/trail.ts +140 -0
  139. package/src/waves/consts.ts +3 -0
  140. package/src/waves/index.ts +4 -0
  141. package/src/waves/layer.ts +167 -0
  142. package/src/waves/simulation.ts +18 -0
  143. package/src/waves/types.ts +9 -0
  144. package/src/wormhole/consts.ts +3 -0
  145. package/src/wormhole/index.ts +4 -0
  146. package/src/wormhole/layer.ts +181 -0
  147. package/src/wormhole/simulation.ts +17 -0
  148. package/src/wormhole/types.ts +10 -0
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,6 @@
1
+ export { FireflyLayer } from './layer';
2
+ export { FireflyParticle, createFireflySprite } from './particle';
3
+ export { FireflySimulation } from './simulation';
4
+ export type { FireflyParticleConfig } from './particle';
5
+ export type { FireflySimulationConfig } from './simulation';
6
+ export type { Firefly } from './types';
@@ -0,0 +1,152 @@
1
+ import { SimulationLayer } from '../layer';
2
+ import { MULBERRY } from './consts';
3
+ import type { FireflySimulationConfig } from './simulation';
4
+ import type { Firefly } from './types';
5
+
6
+ const SPRITE_SIZE = 64;
7
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
8
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
9
+
10
+ export class FireflyLayer extends SimulationLayer {
11
+ readonly #scale: number;
12
+ readonly #size: number;
13
+ readonly #speed: number;
14
+ readonly #glowSpeed: number;
15
+ #maxCount: number;
16
+ #time: number = 0;
17
+ #fireflies: Firefly[] = [];
18
+ #sprite: HTMLCanvasElement;
19
+
20
+ constructor(config: FireflySimulationConfig = {}) {
21
+ super();
22
+
23
+ this.#scale = config.scale ?? 1;
24
+ this.#maxCount = config.count ?? 60;
25
+ this.#size = (config.size ?? 6) * this.#scale;
26
+ this.#speed = config.speed ?? 1;
27
+ this.#glowSpeed = config.glowSpeed ?? 1;
28
+
29
+ const {r, g, b} = this.#parseColor(config.color ?? '#b4ff6a');
30
+
31
+ if (innerWidth < 991) {
32
+ this.#maxCount = Math.floor(this.#maxCount / 2);
33
+ }
34
+
35
+ this.#sprite = this.#createSprite(r, g, b);
36
+
37
+ for (let i = 0; i < this.#maxCount; ++i) {
38
+ this.#fireflies.push(this.#createFirefly());
39
+ }
40
+ }
41
+
42
+ tick(dt: number, _width: number, _height: number): void {
43
+ this.#time += 0.02 * dt * this.#speed;
44
+
45
+ for (const firefly of this.#fireflies) {
46
+ const moveX = Math.sin(this.#time * firefly.freqX1 + firefly.phaseX1) * firefly.amplitudeX
47
+ + Math.sin(this.#time * firefly.freqX2 + firefly.phaseX2) * firefly.amplitudeX * 0.5;
48
+
49
+ const moveY = Math.sin(this.#time * firefly.freqY1 + firefly.phaseY1) * firefly.amplitudeY
50
+ + Math.sin(this.#time * firefly.freqY2 + firefly.phaseY2) * firefly.amplitudeY * 0.5;
51
+
52
+ firefly.x += moveX * dt / (3000 * (1 / this.#speed));
53
+ firefly.y += moveY * dt / (3000 * (1 / this.#speed));
54
+
55
+ if (firefly.x > 1.1) {
56
+ firefly.x = -0.1;
57
+ } else if (firefly.x < -0.1) {
58
+ firefly.x = 1.1;
59
+ }
60
+
61
+ if (firefly.y > 1.1) {
62
+ firefly.y = -0.1;
63
+ } else if (firefly.y < -0.1) {
64
+ firefly.y = 1.1;
65
+ }
66
+ }
67
+ }
68
+
69
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
70
+ ctx.globalCompositeOperation = 'lighter';
71
+
72
+ for (const firefly of this.#fireflies) {
73
+ const alpha = 0.5 + 0.5 * Math.sin(this.#time * firefly.glowSpeed * this.#glowSpeed + firefly.phase);
74
+
75
+ if (alpha < 0.05) {
76
+ continue;
77
+ }
78
+
79
+ const px = firefly.x * width;
80
+ const py = firefly.y * height;
81
+ const displaySize = firefly.size * 2;
82
+
83
+ ctx.globalAlpha = alpha;
84
+ ctx.drawImage(
85
+ this.#sprite,
86
+ px - firefly.size,
87
+ py - firefly.size,
88
+ displaySize,
89
+ displaySize
90
+ );
91
+ }
92
+
93
+ ctx.globalCompositeOperation = 'source-over';
94
+ ctx.globalAlpha = 1;
95
+ }
96
+
97
+ #parseColor(color: string): {r: number; g: number; b: number} {
98
+ const canvas = document.createElement('canvas');
99
+ canvas.width = 1;
100
+ canvas.height = 1;
101
+ const ctx = canvas.getContext('2d')!;
102
+ ctx.fillStyle = color;
103
+ ctx.fillRect(0, 0, 1, 1);
104
+ const data = ctx.getImageData(0, 0, 1, 1).data;
105
+ return {r: data[0], g: data[1], b: data[2]};
106
+ }
107
+
108
+ #createSprite(r: number, g: number, b: number): HTMLCanvasElement {
109
+ const canvas = document.createElement('canvas');
110
+ canvas.width = SPRITE_SIZE;
111
+ canvas.height = SPRITE_SIZE;
112
+ const ctx = canvas.getContext('2d')!;
113
+
114
+ const gradient = ctx.createRadialGradient(
115
+ SPRITE_CENTER, SPRITE_CENTER, 0,
116
+ SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
117
+ );
118
+
119
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1)`);
120
+ gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.8)`);
121
+ gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.3)`);
122
+ gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.08)`);
123
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
124
+
125
+ ctx.fillStyle = gradient;
126
+ ctx.beginPath();
127
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
128
+ ctx.fill();
129
+
130
+ return canvas;
131
+ }
132
+
133
+ #createFirefly(): Firefly {
134
+ return {
135
+ x: MULBERRY.next(),
136
+ y: MULBERRY.next(),
137
+ size: (MULBERRY.next() * 0.6 + 0.4) * this.#size,
138
+ phase: MULBERRY.next() * Math.PI * 2,
139
+ glowSpeed: 0.5 + MULBERRY.next() * 1.5,
140
+ freqX1: 0.3 + MULBERRY.next() * 0.7,
141
+ freqX2: 1.2 + MULBERRY.next() * 1.8,
142
+ freqY1: 0.3 + MULBERRY.next() * 0.7,
143
+ freqY2: 1.2 + MULBERRY.next() * 1.8,
144
+ phaseX1: MULBERRY.next() * Math.PI * 2,
145
+ phaseX2: MULBERRY.next() * Math.PI * 2,
146
+ phaseY1: MULBERRY.next() * Math.PI * 2,
147
+ phaseY2: MULBERRY.next() * Math.PI * 2,
148
+ amplitudeX: 0.3 + MULBERRY.next() * 0.7,
149
+ amplitudeY: 0.3 + MULBERRY.next() * 0.7
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,124 @@
1
+ export interface FireflyParticleConfig {
2
+ readonly glowSpeed?: number;
3
+ readonly scale?: number;
4
+ readonly size?: number;
5
+ readonly speed?: number;
6
+ }
7
+
8
+ export function createFireflySprite(color: string, size: number = 64): HTMLCanvasElement {
9
+ const canvas = document.createElement('canvas');
10
+ canvas.width = size;
11
+ canvas.height = size;
12
+
13
+ const ctx = canvas.getContext('2d')!;
14
+ const center = size / 2;
15
+ const radius = size / 2;
16
+
17
+ const tmp = document.createElement('canvas');
18
+ tmp.width = 1;
19
+ tmp.height = 1;
20
+ const tmpCtx = tmp.getContext('2d')!;
21
+ tmpCtx.fillStyle = color;
22
+ tmpCtx.fillRect(0, 0, 1, 1);
23
+ const [r, g, b] = tmpCtx.getImageData(0, 0, 1, 1).data;
24
+
25
+ const gradient = ctx.createRadialGradient(center, center, 0, center, center, radius);
26
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1)`);
27
+ gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.8)`);
28
+ gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.3)`);
29
+ gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.08)`);
30
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
31
+
32
+ ctx.fillStyle = gradient;
33
+ ctx.beginPath();
34
+ ctx.arc(center, center, radius, 0, Math.PI * 2);
35
+ ctx.fill();
36
+
37
+ return canvas;
38
+ }
39
+
40
+ export class FireflyParticle {
41
+ readonly #sprite: HTMLCanvasElement;
42
+ readonly #bounds: { width: number; height: number };
43
+ readonly #glowSpeed: number;
44
+ readonly #size: number;
45
+ readonly #speed: number;
46
+ readonly #phase: number;
47
+ readonly #freqX1: number;
48
+ readonly #freqX2: number;
49
+ readonly #freqY1: number;
50
+ readonly #freqY2: number;
51
+ readonly #phaseX1: number;
52
+ readonly #phaseX2: number;
53
+ readonly #phaseY1: number;
54
+ readonly #phaseY2: number;
55
+ readonly #amplitudeX: number;
56
+ readonly #amplitudeY: number;
57
+ #x: number;
58
+ #y: number;
59
+ #time: number = 0;
60
+
61
+ get position(): { x: number; y: number } {
62
+ return {x: this.#x, y: this.#y};
63
+ }
64
+
65
+ constructor(x: number, y: number, bounds: { width: number; height: number }, sprite: HTMLCanvasElement, config: FireflyParticleConfig = {}) {
66
+ this.#x = x;
67
+ this.#y = y;
68
+ this.#bounds = bounds;
69
+ this.#sprite = sprite;
70
+ this.#glowSpeed = config.glowSpeed ?? (0.5 + Math.random() * 1.5);
71
+ this.#size = (config.size ?? 6) * (config.scale ?? 1);
72
+ this.#speed = config.speed ?? 1;
73
+ this.#phase = Math.random() * Math.PI * 2;
74
+ this.#freqX1 = 0.3 + Math.random() * 0.7;
75
+ this.#freqX2 = 1.2 + Math.random() * 1.8;
76
+ this.#freqY1 = 0.3 + Math.random() * 0.7;
77
+ this.#freqY2 = 1.2 + Math.random() * 1.8;
78
+ this.#phaseX1 = Math.random() * Math.PI * 2;
79
+ this.#phaseX2 = Math.random() * Math.PI * 2;
80
+ this.#phaseY1 = Math.random() * Math.PI * 2;
81
+ this.#phaseY2 = Math.random() * Math.PI * 2;
82
+ this.#amplitudeX = 0.3 + Math.random() * 0.7;
83
+ this.#amplitudeY = 0.3 + Math.random() * 0.7;
84
+ }
85
+
86
+ draw(ctx: CanvasRenderingContext2D): void {
87
+ const alpha = 0.5 + 0.5 * Math.sin(this.#time * this.#glowSpeed + this.#phase);
88
+
89
+ if (alpha < 0.05) {
90
+ return;
91
+ }
92
+
93
+ const displaySize = this.#size * 2;
94
+
95
+ ctx.globalAlpha = alpha;
96
+ ctx.drawImage(this.#sprite, this.#x - this.#size, this.#y - this.#size, displaySize, displaySize);
97
+ ctx.globalAlpha = 1;
98
+ }
99
+
100
+ tick(dt: number = 1): void {
101
+ this.#time += 0.02 * dt * this.#speed;
102
+
103
+ const moveX = Math.sin(this.#time * this.#freqX1 + this.#phaseX1) * this.#amplitudeX * this.#bounds.width
104
+ + Math.sin(this.#time * this.#freqX2 + this.#phaseX2) * this.#amplitudeX * this.#bounds.width * 0.5;
105
+
106
+ const moveY = Math.sin(this.#time * this.#freqY1 + this.#phaseY1) * this.#amplitudeY * this.#bounds.height
107
+ + Math.sin(this.#time * this.#freqY2 + this.#phaseY2) * this.#amplitudeY * this.#bounds.height * 0.5;
108
+
109
+ this.#x += (moveX / 3000) * dt;
110
+ this.#y += (moveY / 3000) * dt;
111
+
112
+ if (this.#x > this.#bounds.width + this.#size) {
113
+ this.#x = -this.#size;
114
+ } else if (this.#x < -this.#size) {
115
+ this.#x = this.#bounds.width + this.#size;
116
+ }
117
+
118
+ if (this.#y > this.#bounds.height + this.#size) {
119
+ this.#y = -this.#size;
120
+ } else if (this.#y < -this.#size) {
121
+ this.#y = this.#bounds.height + this.#size;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,18 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { FireflyLayer } from './layer';
3
+
4
+ export interface FireflySimulationConfig {
5
+ readonly count?: number;
6
+ readonly color?: string;
7
+ readonly size?: number;
8
+ readonly speed?: number;
9
+ readonly glowSpeed?: number;
10
+ readonly scale?: number;
11
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
12
+ }
13
+
14
+ export class FireflySimulation extends SimulationCanvas {
15
+ constructor(canvas: HTMLCanvasElement, config: FireflySimulationConfig = {}) {
16
+ super(canvas, new FireflyLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
17
+ }
18
+ }
@@ -0,0 +1,17 @@
1
+ export type Firefly = {
2
+ x: number;
3
+ y: number;
4
+ size: number;
5
+ phase: number;
6
+ glowSpeed: number;
7
+ freqX1: number;
8
+ freqX2: number;
9
+ freqY1: number;
10
+ freqY2: number;
11
+ phaseX1: number;
12
+ phaseX2: number;
13
+ phaseY1: number;
14
+ phaseY2: number;
15
+ amplitudeX: number;
16
+ amplitudeY: number;
17
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,4 @@
1
+ export { FirepitLayer } from './layer';
2
+ export { FirepitSimulation } from './simulation';
3
+ export type { FirepitSimulationConfig } from './simulation';
4
+ export type { Ember, FlameLayer } from './types';
@@ -0,0 +1,174 @@
1
+ import { SimulationLayer } from '../layer';
2
+ import { MULBERRY } from './consts';
3
+ import type { FirepitSimulationConfig } from './simulation';
4
+ import type { Ember, FlameLayer } from './types';
5
+
6
+ export class FirepitLayer extends SimulationLayer {
7
+ readonly #scale: number;
8
+ readonly #flameWidth: number;
9
+ readonly #flameHeight: number;
10
+ readonly #intensity: number;
11
+ #maxEmbers: number;
12
+ #time: number = 0;
13
+ #embers: Ember[] = [];
14
+ #flameLayers: FlameLayer[] = [];
15
+
16
+ constructor(config: FirepitSimulationConfig = {}) {
17
+ super();
18
+
19
+ this.#scale = config.scale ?? 1;
20
+ this.#maxEmbers = config.embers ?? 60;
21
+ this.#flameWidth = config.flameWidth ?? 0.4;
22
+ this.#flameHeight = config.flameHeight ?? 0.35;
23
+ this.#intensity = config.intensity ?? 1;
24
+
25
+ if (innerWidth < 991) {
26
+ this.#maxEmbers = Math.floor(this.#maxEmbers / 2);
27
+ }
28
+
29
+ for (let i = 0; i < 5; i++) {
30
+ this.#flameLayers.push({
31
+ x: 0.5 + (MULBERRY.next() - 0.5) * 0.1,
32
+ phase: MULBERRY.next() * Math.PI * 2,
33
+ speed: 1.5 + MULBERRY.next() * 2,
34
+ amplitude: 0.02 + MULBERRY.next() * 0.03,
35
+ width: this.#flameWidth * (0.6 + MULBERRY.next() * 0.4),
36
+ height: this.#flameHeight * (0.7 + MULBERRY.next() * 0.3)
37
+ });
38
+ }
39
+ }
40
+
41
+ tick(dt: number, _width: number, _height: number): void {
42
+ this.#time += 0.03 * dt * this.#intensity;
43
+
44
+ if (this.#embers.length < this.#maxEmbers && MULBERRY.next() < 0.3 * this.#intensity * dt) {
45
+ this.#embers.push(this.#createEmber());
46
+ }
47
+
48
+ let alive = 0;
49
+
50
+ for (let i = 0; i < this.#embers.length; i++) {
51
+ const ember = this.#embers[i];
52
+
53
+ ember.x += ember.vx * dt;
54
+ ember.y += ember.vy * dt;
55
+ ember.vx += (MULBERRY.next() - 0.5) * 0.0002 * dt;
56
+ ember.vy -= 0.00005 * dt;
57
+ ember.life -= dt;
58
+ ember.flicker = 0.6 + Math.sin(this.#time * 8 + ember.brightness * 20) * 0.4;
59
+
60
+ if (ember.life > 0) {
61
+ this.#embers[alive++] = ember;
62
+ }
63
+ }
64
+
65
+ this.#embers.length = alive;
66
+ }
67
+
68
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
69
+ this.#drawFlames(ctx, width, height);
70
+ this.#drawEmbers(ctx, width, height);
71
+ }
72
+
73
+ #drawFlames(ctx: CanvasRenderingContext2D, width: number, height: number): void {
74
+ const baseY = height * 0.85;
75
+ const centerX = width * 0.5;
76
+
77
+ ctx.globalCompositeOperation = 'lighter';
78
+
79
+ for (let layerIndex = 0; layerIndex < this.#flameLayers.length; layerIndex++) {
80
+ const layer = this.#flameLayers[layerIndex];
81
+ const wobbleX = Math.sin(this.#time * layer.speed + layer.phase) * layer.amplitude * width;
82
+ const flameW = layer.width * width * this.#scale * 0.5;
83
+ const flameH = layer.height * height * this.#scale;
84
+
85
+ const gradient = ctx.createRadialGradient(
86
+ centerX + wobbleX, baseY, 0,
87
+ centerX + wobbleX, baseY - flameH * 0.5, flameH * 0.6
88
+ );
89
+
90
+ const alphaBase = (0.15 + layerIndex * 0.03) * this.#intensity;
91
+ const flicker = 0.85 + Math.sin(this.#time * (3 + layerIndex) + layer.phase) * 0.15;
92
+ const alpha = alphaBase * flicker;
93
+
94
+ if (layerIndex < 2) {
95
+ gradient.addColorStop(0, `rgba(255, 255, 200, ${alpha})`);
96
+ gradient.addColorStop(0.3, `rgba(255, 180, 50, ${alpha * 0.8})`);
97
+ gradient.addColorStop(0.6, `rgba(255, 100, 20, ${alpha * 0.5})`);
98
+ gradient.addColorStop(1, `rgba(200, 30, 0, 0)`);
99
+ } else {
100
+ gradient.addColorStop(0, `rgba(255, 200, 80, ${alpha * 0.7})`);
101
+ gradient.addColorStop(0.4, `rgba(255, 120, 30, ${alpha * 0.5})`);
102
+ gradient.addColorStop(1, `rgba(180, 40, 0, 0)`);
103
+ }
104
+
105
+ ctx.fillStyle = gradient;
106
+ ctx.beginPath();
107
+ ctx.ellipse(centerX + wobbleX, baseY, flameW, flameH, 0, 0, Math.PI * 2);
108
+ ctx.fill();
109
+ }
110
+
111
+ const glowGradient = ctx.createRadialGradient(
112
+ centerX, height * 0.85, 0,
113
+ centerX, height * 0.85, width * 0.35 * this.#scale
114
+ );
115
+ const glowAlpha = 0.06 * this.#intensity * (0.9 + Math.sin(this.#time * 2) * 0.1);
116
+ glowGradient.addColorStop(0, `rgba(255, 150, 50, ${glowAlpha})`);
117
+ glowGradient.addColorStop(0.5, `rgba(255, 80, 20, ${glowAlpha * 0.3})`);
118
+ glowGradient.addColorStop(1, 'rgba(255, 50, 0, 0)');
119
+ ctx.fillStyle = glowGradient;
120
+ ctx.fillRect(0, 0, width, height);
121
+
122
+ ctx.globalCompositeOperation = 'source-over';
123
+ }
124
+
125
+ #drawEmbers(ctx: CanvasRenderingContext2D, width: number, height: number): void {
126
+ ctx.globalCompositeOperation = 'lighter';
127
+
128
+ for (const ember of this.#embers) {
129
+ const px = ember.x * width;
130
+ const py = ember.y * height;
131
+ const lifeRatio = ember.life / ember.maxLife;
132
+ const alpha = lifeRatio * ember.flicker * this.#intensity;
133
+ const size = ember.size * this.#scale * (0.5 + lifeRatio * 0.5);
134
+
135
+ if (alpha < 0.02) {
136
+ continue;
137
+ }
138
+
139
+ const gradient = ctx.createRadialGradient(px, py, 0, px, py, size * 3);
140
+ gradient.addColorStop(0, `rgba(255, ${180 + ember.brightness * 75}, ${50 + ember.brightness * 100}, ${alpha})`);
141
+ gradient.addColorStop(0.3, `rgba(255, ${120 + ember.brightness * 50}, 20, ${alpha * 0.5})`);
142
+ gradient.addColorStop(1, `rgba(255, 80, 0, 0)`);
143
+
144
+ ctx.fillStyle = gradient;
145
+ ctx.beginPath();
146
+ ctx.arc(px, py, size * 3, 0, Math.PI * 2);
147
+ ctx.fill();
148
+
149
+ ctx.fillStyle = `rgba(255, ${200 + ember.brightness * 55}, ${100 + ember.brightness * 100}, ${alpha * 0.8})`;
150
+ ctx.beginPath();
151
+ ctx.arc(px, py, size * 0.5, 0, Math.PI * 2);
152
+ ctx.fill();
153
+ }
154
+
155
+ ctx.globalCompositeOperation = 'source-over';
156
+ }
157
+
158
+ #createEmber(): Ember {
159
+ const baseY = 0.85;
160
+ const maxLife = 60 + MULBERRY.next() * 120;
161
+
162
+ return {
163
+ x: 0.5 + (MULBERRY.next() - 0.5) * this.#flameWidth * 0.6,
164
+ y: baseY - MULBERRY.next() * this.#flameHeight * 0.3,
165
+ vx: (MULBERRY.next() - 0.5) * 0.002,
166
+ vy: -(0.001 + MULBERRY.next() * 0.003),
167
+ size: (1 + MULBERRY.next() * 2.5) * this.#scale,
168
+ life: maxLife,
169
+ maxLife,
170
+ brightness: MULBERRY.next(),
171
+ flicker: 1
172
+ };
173
+ }
174
+ }
@@ -0,0 +1,17 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { FirepitLayer } from './layer';
3
+
4
+ export interface FirepitSimulationConfig {
5
+ readonly embers?: number;
6
+ readonly flameWidth?: number;
7
+ readonly flameHeight?: number;
8
+ readonly intensity?: number;
9
+ readonly scale?: number;
10
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
11
+ }
12
+
13
+ export class FirepitSimulation extends SimulationCanvas {
14
+ constructor(canvas: HTMLCanvasElement, config: FirepitSimulationConfig = {}) {
15
+ super(canvas, new FirepitLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ export type Ember = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ size: number;
7
+ life: number;
8
+ maxLife: number;
9
+ brightness: number;
10
+ flicker: number;
11
+ };
12
+
13
+ export type FlameLayer = {
14
+ x: number;
15
+ phase: number;
16
+ speed: number;
17
+ amplitude: number;
18
+ width: number;
19
+ height: number;
20
+ };
@@ -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,7 @@
1
+ export { Explosion } from './explosion';
2
+ export { Firework } from './firework';
3
+ export { FireworkLayer } from './layer';
4
+ export { Spark } from './spark';
1
5
  export * from './simulation';
2
- export { FIREWORK_VARIANTS } from './types';
3
- export type { ExplosionType, FireworkSimulationConfig, FireworkVariant, ParticleShape } from './types';
6
+ export { EXPLOSION_CONFIGS, FIREWORK_VARIANTS } from './types';
7
+ export type { ExplosionConfig, ExplosionType, FireworkSimulationConfig, FireworkVariant, ParticleShape } from './types';