@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,169 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { Firefly } from './types';
4
+
5
+ const SPRITE_SIZE = 64;
6
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
7
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
8
+
9
+ export interface FirefliesConfig {
10
+ readonly count?: number;
11
+ readonly color?: string;
12
+ readonly size?: number;
13
+ readonly speed?: number;
14
+ readonly glowSpeed?: number;
15
+ readonly scale?: number;
16
+ }
17
+
18
+ export class Fireflies extends Effect<FirefliesConfig> {
19
+ readonly #scale: number;
20
+ readonly #size: number;
21
+ #speed: number;
22
+ #glowSpeed: number;
23
+ #maxCount: number;
24
+ #time: number = 0;
25
+ #fireflies: Firefly[] = [];
26
+ #sprite: HTMLCanvasElement;
27
+
28
+ constructor(config: FirefliesConfig = {}) {
29
+ super();
30
+
31
+ this.#scale = config.scale ?? 1;
32
+ this.#maxCount = config.count ?? 60;
33
+ this.#size = (config.size ?? 6) * this.#scale;
34
+ this.#speed = config.speed ?? 1;
35
+ this.#glowSpeed = config.glowSpeed ?? 1;
36
+
37
+ const {r, g, b} = this.#parseColor(config.color ?? '#b4ff6a');
38
+
39
+ if (innerWidth < 991) {
40
+ this.#maxCount = Math.floor(this.#maxCount / 2);
41
+ }
42
+
43
+ this.#sprite = this.#createSprite(r, g, b);
44
+
45
+ for (let i = 0; i < this.#maxCount; ++i) {
46
+ this.#fireflies.push(this.#createFirefly());
47
+ }
48
+ }
49
+
50
+ configure(config: Partial<FirefliesConfig>): void {
51
+ if (config.speed !== undefined) {
52
+ this.#speed = config.speed;
53
+ }
54
+ if (config.glowSpeed !== undefined) {
55
+ this.#glowSpeed = config.glowSpeed;
56
+ }
57
+ }
58
+
59
+ tick(dt: number, _width: number, _height: number): void {
60
+ this.#time += 0.02 * dt * this.#speed;
61
+
62
+ for (const firefly of this.#fireflies) {
63
+ const moveX = Math.sin(this.#time * firefly.freqX1 + firefly.phaseX1) * firefly.amplitudeX
64
+ + Math.sin(this.#time * firefly.freqX2 + firefly.phaseX2) * firefly.amplitudeX * 0.5;
65
+
66
+ const moveY = Math.sin(this.#time * firefly.freqY1 + firefly.phaseY1) * firefly.amplitudeY
67
+ + Math.sin(this.#time * firefly.freqY2 + firefly.phaseY2) * firefly.amplitudeY * 0.5;
68
+
69
+ firefly.x += moveX * dt / (3000 * (1 / this.#speed));
70
+ firefly.y += moveY * dt / (3000 * (1 / this.#speed));
71
+
72
+ if (firefly.x > 1.1) {
73
+ firefly.x = -0.1;
74
+ } else if (firefly.x < -0.1) {
75
+ firefly.x = 1.1;
76
+ }
77
+
78
+ if (firefly.y > 1.1) {
79
+ firefly.y = -0.1;
80
+ } else if (firefly.y < -0.1) {
81
+ firefly.y = 1.1;
82
+ }
83
+ }
84
+ }
85
+
86
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
87
+ ctx.globalCompositeOperation = 'lighter';
88
+
89
+ for (const firefly of this.#fireflies) {
90
+ const alpha = 0.5 + 0.5 * Math.sin(this.#time * firefly.glowSpeed * this.#glowSpeed + firefly.phase);
91
+
92
+ if (alpha < 0.05) {
93
+ continue;
94
+ }
95
+
96
+ const px = firefly.x * width;
97
+ const py = firefly.y * height;
98
+ const displaySize = firefly.size * 2;
99
+
100
+ ctx.globalAlpha = alpha;
101
+ ctx.drawImage(
102
+ this.#sprite,
103
+ px - firefly.size,
104
+ py - firefly.size,
105
+ displaySize,
106
+ displaySize
107
+ );
108
+ }
109
+
110
+ ctx.globalCompositeOperation = 'source-over';
111
+ ctx.globalAlpha = 1;
112
+ }
113
+
114
+ #parseColor(color: string): { r: number; g: number; b: number } {
115
+ const canvas = document.createElement('canvas');
116
+ canvas.width = 1;
117
+ canvas.height = 1;
118
+ const ctx = canvas.getContext('2d')!;
119
+ ctx.fillStyle = color;
120
+ ctx.fillRect(0, 0, 1, 1);
121
+ const data = ctx.getImageData(0, 0, 1, 1).data;
122
+ return {r: data[0], g: data[1], b: data[2]};
123
+ }
124
+
125
+ #createSprite(r: number, g: number, b: number): HTMLCanvasElement {
126
+ const canvas = document.createElement('canvas');
127
+ canvas.width = SPRITE_SIZE;
128
+ canvas.height = SPRITE_SIZE;
129
+ const ctx = canvas.getContext('2d')!;
130
+
131
+ const gradient = ctx.createRadialGradient(
132
+ SPRITE_CENTER, SPRITE_CENTER, 0,
133
+ SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
134
+ );
135
+
136
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1)`);
137
+ gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.8)`);
138
+ gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.3)`);
139
+ gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.08)`);
140
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
141
+
142
+ ctx.fillStyle = gradient;
143
+ ctx.beginPath();
144
+ ctx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
145
+ ctx.fill();
146
+
147
+ return canvas;
148
+ }
149
+
150
+ #createFirefly(): Firefly {
151
+ return {
152
+ x: MULBERRY.next(),
153
+ y: MULBERRY.next(),
154
+ size: (MULBERRY.next() * 0.6 + 0.4) * this.#size,
155
+ phase: MULBERRY.next() * Math.PI * 2,
156
+ glowSpeed: 0.5 + MULBERRY.next() * 1.5,
157
+ freqX1: 0.3 + MULBERRY.next() * 0.7,
158
+ freqX2: 1.2 + MULBERRY.next() * 1.8,
159
+ freqY1: 0.3 + MULBERRY.next() * 0.7,
160
+ freqY2: 1.2 + MULBERRY.next() * 1.8,
161
+ phaseX1: MULBERRY.next() * Math.PI * 2,
162
+ phaseX2: MULBERRY.next() * Math.PI * 2,
163
+ phaseY1: MULBERRY.next() * Math.PI * 2,
164
+ phaseY2: MULBERRY.next() * Math.PI * 2,
165
+ amplitudeX: 0.3 + MULBERRY.next() * 0.7,
166
+ amplitudeY: 0.3 + MULBERRY.next() * 0.7
167
+ };
168
+ }
169
+ }
@@ -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,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,10 @@
1
+ import { Firepit } from './layer';
2
+ import type { FirepitConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createFirepit(config?: FirepitConfig): Effect<FirepitConfig> {
6
+ return new Firepit(config);
7
+ }
8
+
9
+ export type { FirepitConfig };
10
+ export type { Ember, FlameLayer } from './types';
@@ -0,0 +1,193 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { Ember, FlameLayer } from './types';
4
+
5
+ export interface FirepitConfig {
6
+ readonly embers?: number;
7
+ readonly flameWidth?: number;
8
+ readonly flameHeight?: number;
9
+ readonly intensity?: number;
10
+ readonly scale?: number;
11
+ }
12
+
13
+ export class Firepit extends Effect<FirepitConfig> {
14
+ readonly #scale: number;
15
+ #flameWidth: number;
16
+ #flameHeight: number;
17
+ #intensity: number;
18
+ #maxEmbers: number;
19
+ #time: number = 0;
20
+ #embers: Ember[] = [];
21
+ #flameLayers: FlameLayer[] = [];
22
+
23
+ constructor(config: FirepitConfig = {}) {
24
+ super();
25
+
26
+ this.#scale = config.scale ?? 1;
27
+ this.#maxEmbers = config.embers ?? 60;
28
+ this.#flameWidth = config.flameWidth ?? 0.4;
29
+ this.#flameHeight = config.flameHeight ?? 0.35;
30
+ this.#intensity = config.intensity ?? 1;
31
+
32
+ if (innerWidth < 991) {
33
+ this.#maxEmbers = Math.floor(this.#maxEmbers / 2);
34
+ }
35
+
36
+ for (let i = 0; i < 5; i++) {
37
+ this.#flameLayers.push({
38
+ x: 0.5 + (MULBERRY.next() - 0.5) * 0.1,
39
+ phase: MULBERRY.next() * Math.PI * 2,
40
+ speed: 1.5 + MULBERRY.next() * 2,
41
+ amplitude: 0.02 + MULBERRY.next() * 0.03,
42
+ width: this.#flameWidth * (0.6 + MULBERRY.next() * 0.4),
43
+ height: this.#flameHeight * (0.7 + MULBERRY.next() * 0.3)
44
+ });
45
+ }
46
+ }
47
+
48
+ configure(config: Partial<FirepitConfig>): void {
49
+ if (config.intensity !== undefined) {
50
+ this.#intensity = config.intensity;
51
+ }
52
+ if (config.flameWidth !== undefined) {
53
+ this.#flameWidth = config.flameWidth;
54
+ }
55
+ if (config.flameHeight !== undefined) {
56
+ this.#flameHeight = config.flameHeight;
57
+ }
58
+ }
59
+
60
+ tick(dt: number, _width: number, _height: number): void {
61
+ this.#time += 0.03 * dt * this.#intensity;
62
+
63
+ if (this.#embers.length < this.#maxEmbers && MULBERRY.next() < 0.3 * this.#intensity * dt) {
64
+ this.#embers.push(this.#createEmber());
65
+ }
66
+
67
+ let alive = 0;
68
+
69
+ for (let i = 0; i < this.#embers.length; i++) {
70
+ const ember = this.#embers[i];
71
+
72
+ ember.x += ember.vx * dt;
73
+ ember.y += ember.vy * dt;
74
+ ember.vx += (MULBERRY.next() - 0.5) * 0.0002 * dt;
75
+ ember.vy -= 0.00005 * dt;
76
+ ember.life -= dt;
77
+ ember.flicker = 0.6 + Math.sin(this.#time * 8 + ember.brightness * 20) * 0.4;
78
+
79
+ if (ember.life > 0) {
80
+ this.#embers[alive++] = ember;
81
+ }
82
+ }
83
+
84
+ this.#embers.length = alive;
85
+ }
86
+
87
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
88
+ this.#drawFlames(ctx, width, height);
89
+ this.#drawEmbers(ctx, width, height);
90
+ }
91
+
92
+ #drawFlames(ctx: CanvasRenderingContext2D, width: number, height: number): void {
93
+ const baseY = height * 0.85;
94
+ const centerX = width * 0.5;
95
+
96
+ ctx.globalCompositeOperation = 'lighter';
97
+
98
+ for (let layerIndex = 0; layerIndex < this.#flameLayers.length; layerIndex++) {
99
+ const layer = this.#flameLayers[layerIndex];
100
+ const wobbleX = Math.sin(this.#time * layer.speed + layer.phase) * layer.amplitude * width;
101
+ const flameW = layer.width * width * this.#scale * 0.5;
102
+ const flameH = layer.height * height * this.#scale;
103
+
104
+ const gradient = ctx.createRadialGradient(
105
+ centerX + wobbleX, baseY, 0,
106
+ centerX + wobbleX, baseY - flameH * 0.5, flameH * 0.6
107
+ );
108
+
109
+ const alphaBase = (0.15 + layerIndex * 0.03) * this.#intensity;
110
+ const flicker = 0.85 + Math.sin(this.#time * (3 + layerIndex) + layer.phase) * 0.15;
111
+ const alpha = alphaBase * flicker;
112
+
113
+ if (layerIndex < 2) {
114
+ gradient.addColorStop(0, `rgba(255, 255, 200, ${alpha})`);
115
+ gradient.addColorStop(0.3, `rgba(255, 180, 50, ${alpha * 0.8})`);
116
+ gradient.addColorStop(0.6, `rgba(255, 100, 20, ${alpha * 0.5})`);
117
+ gradient.addColorStop(1, `rgba(200, 30, 0, 0)`);
118
+ } else {
119
+ gradient.addColorStop(0, `rgba(255, 200, 80, ${alpha * 0.7})`);
120
+ gradient.addColorStop(0.4, `rgba(255, 120, 30, ${alpha * 0.5})`);
121
+ gradient.addColorStop(1, `rgba(180, 40, 0, 0)`);
122
+ }
123
+
124
+ ctx.fillStyle = gradient;
125
+ ctx.beginPath();
126
+ ctx.ellipse(centerX + wobbleX, baseY, flameW, flameH, 0, 0, Math.PI * 2);
127
+ ctx.fill();
128
+ }
129
+
130
+ const glowGradient = ctx.createRadialGradient(
131
+ centerX, height * 0.85, 0,
132
+ centerX, height * 0.85, width * 0.35 * this.#scale
133
+ );
134
+ const glowAlpha = 0.06 * this.#intensity * (0.9 + Math.sin(this.#time * 2) * 0.1);
135
+ glowGradient.addColorStop(0, `rgba(255, 150, 50, ${glowAlpha})`);
136
+ glowGradient.addColorStop(0.5, `rgba(255, 80, 20, ${glowAlpha * 0.3})`);
137
+ glowGradient.addColorStop(1, 'rgba(255, 50, 0, 0)');
138
+ ctx.fillStyle = glowGradient;
139
+ ctx.fillRect(0, 0, width, height);
140
+
141
+ ctx.globalCompositeOperation = 'source-over';
142
+ }
143
+
144
+ #drawEmbers(ctx: CanvasRenderingContext2D, width: number, height: number): void {
145
+ ctx.globalCompositeOperation = 'lighter';
146
+
147
+ for (const ember of this.#embers) {
148
+ const px = ember.x * width;
149
+ const py = ember.y * height;
150
+ const lifeRatio = ember.life / ember.maxLife;
151
+ const alpha = lifeRatio * ember.flicker * this.#intensity;
152
+ const size = ember.size * this.#scale * (0.5 + lifeRatio * 0.5);
153
+
154
+ if (alpha < 0.02) {
155
+ continue;
156
+ }
157
+
158
+ const gradient = ctx.createRadialGradient(px, py, 0, px, py, size * 3);
159
+ gradient.addColorStop(0, `rgba(255, ${180 + ember.brightness * 75}, ${50 + ember.brightness * 100}, ${alpha})`);
160
+ gradient.addColorStop(0.3, `rgba(255, ${120 + ember.brightness * 50}, 20, ${alpha * 0.5})`);
161
+ gradient.addColorStop(1, `rgba(255, 80, 0, 0)`);
162
+
163
+ ctx.fillStyle = gradient;
164
+ ctx.beginPath();
165
+ ctx.arc(px, py, size * 3, 0, Math.PI * 2);
166
+ ctx.fill();
167
+
168
+ ctx.fillStyle = `rgba(255, ${200 + ember.brightness * 55}, ${100 + ember.brightness * 100}, ${alpha * 0.8})`;
169
+ ctx.beginPath();
170
+ ctx.arc(px, py, size * 0.5, 0, Math.PI * 2);
171
+ ctx.fill();
172
+ }
173
+
174
+ ctx.globalCompositeOperation = 'source-over';
175
+ }
176
+
177
+ #createEmber(): Ember {
178
+ const baseY = 0.85;
179
+ const maxLife = 60 + MULBERRY.next() * 120;
180
+
181
+ return {
182
+ x: 0.5 + (MULBERRY.next() - 0.5) * this.#flameWidth * 0.6,
183
+ y: baseY - MULBERRY.next() * this.#flameHeight * 0.3,
184
+ vx: (MULBERRY.next() - 0.5) * 0.002,
185
+ vy: -(0.001 + MULBERRY.next() * 0.003),
186
+ size: (1 + MULBERRY.next() * 2.5) * this.#scale,
187
+ life: maxLife,
188
+ maxLife,
189
+ brightness: MULBERRY.next(),
190
+ flicker: 1
191
+ };
192
+ }
193
+ }
@@ -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
+ };