@basmilius/sparkle 2.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 +2 -1
  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,5 @@
1
+ export type PlasmaColor = {
2
+ r: number;
3
+ g: number;
4
+ b: number;
5
+ };
@@ -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 { RainLayer } from './layer';
2
+ export { RaindropParticle, SplashParticle } from './particle';
3
+ export { RainSimulation } from './simulation';
4
+ export type { RaindropParticleConfig, SplashParticleConfig } from './particle';
5
+ export type { RainSimulationConfig } from './simulation';
6
+ export type { Raindrop, RainVariant, Splash } from './types';
@@ -0,0 +1,172 @@
1
+ import { parseColor } from '../color';
2
+ import { SimulationLayer } from '../layer';
3
+ import { MULBERRY } from './consts';
4
+ import type { RainSimulationConfig } from './simulation';
5
+ import type { Raindrop, RainVariant, Splash } from './types';
6
+
7
+ const VARIANT_PRESETS: Record<RainVariant, {drops: number; speed: number; wind: number; splashes: boolean}> = {
8
+ drizzle: {drops: 70, speed: 0.55, wind: 0.1, splashes: false},
9
+ downpour: {drops: 200, speed: 0.85, wind: 0.25, splashes: true},
10
+ thunderstorm: {drops: 300, speed: 1, wind: 0.4, splashes: true}
11
+ };
12
+
13
+ export class RainLayer extends SimulationLayer {
14
+ readonly #scale: number;
15
+ readonly #speed: number;
16
+ readonly #wind: number;
17
+ readonly #groundLevel: number;
18
+ readonly #enableSplashes: boolean;
19
+ readonly #colorR: number;
20
+ readonly #colorG: number;
21
+ readonly #colorB: number;
22
+ #maxDrops: number;
23
+ #drops: Raindrop[] = [];
24
+ #splashes: Splash[] = [];
25
+
26
+ constructor(config: RainSimulationConfig = {}) {
27
+ super();
28
+
29
+ const variant = config.variant ?? 'downpour';
30
+ const preset = VARIANT_PRESETS[variant];
31
+
32
+ this.#scale = config.scale ?? 1;
33
+ this.#maxDrops = config.drops ?? preset.drops;
34
+ this.#speed = config.speed ?? preset.speed;
35
+ this.#wind = config.wind ?? preset.wind;
36
+ this.#groundLevel = config.groundLevel ?? 1.0;
37
+ this.#enableSplashes = config.splashes ?? preset.splashes;
38
+
39
+ const {r, g, b} = parseColor(config.color ?? 'rgba(174, 194, 224, 0.6)');
40
+ this.#colorR = r;
41
+ this.#colorG = g;
42
+ this.#colorB = b;
43
+
44
+ if (innerWidth < 991) {
45
+ this.#maxDrops = Math.floor(this.#maxDrops / 2);
46
+ }
47
+
48
+ for (let i = 0; i < this.#maxDrops; ++i) {
49
+ this.#drops.push(this.#createDrop(true));
50
+ }
51
+ }
52
+
53
+ tick(dt: number, width: number, height: number): void {
54
+ // Update raindrops
55
+ let aliveDrops = 0;
56
+
57
+ for (let i = 0; i < this.#drops.length; i++) {
58
+ const drop = this.#drops[i];
59
+
60
+ drop.x += (drop.vx * dt) / width;
61
+ drop.y += (drop.vy * dt) / height;
62
+
63
+ if (drop.y >= this.#groundLevel) {
64
+ if (this.#enableSplashes) {
65
+ this.#createSplashBurst(drop.x, this.#groundLevel);
66
+ }
67
+
68
+ this.#drops[aliveDrops++] = this.#createDrop(false);
69
+ } else {
70
+ this.#drops[aliveDrops++] = drop;
71
+ }
72
+ }
73
+
74
+ this.#drops.length = aliveDrops;
75
+
76
+ // Refill drops
77
+ while (this.#drops.length < this.#maxDrops) {
78
+ this.#drops.push(this.#createDrop(false));
79
+ }
80
+
81
+ // Update splashes
82
+ let aliveSplashes = 0;
83
+
84
+ for (let i = 0; i < this.#splashes.length; i++) {
85
+ const splash = this.#splashes[i];
86
+
87
+ splash.x += (splash.vx * dt) / width;
88
+ splash.y += (splash.vy * dt) / height;
89
+ splash.vy += splash.gravity * dt;
90
+ splash.alpha -= 0.04 * dt;
91
+
92
+ if (splash.alpha > 0) {
93
+ this.#splashes[aliveSplashes++] = splash;
94
+ }
95
+ }
96
+
97
+ this.#splashes.length = aliveSplashes;
98
+ }
99
+
100
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
101
+ // Raindrops
102
+ ctx.lineCap = 'round';
103
+ for (const drop of this.#drops) {
104
+ const px = drop.x * width;
105
+ const py = drop.y * height;
106
+ const lineWidth = (0.4 + drop.depth * 1) * this.#scale;
107
+
108
+ // Draw line along velocity direction
109
+ const speed = Math.sqrt(drop.vx * drop.vx + drop.vy * drop.vy);
110
+ const nx = speed > 0 ? drop.vx / speed : 0;
111
+ const ny = speed > 0 ? drop.vy / speed : -1;
112
+ const tailLength = drop.length * drop.depth;
113
+
114
+ ctx.beginPath();
115
+ ctx.moveTo(px, py);
116
+ ctx.lineTo(px - nx * tailLength, py - ny * tailLength);
117
+ ctx.strokeStyle = `rgba(${this.#colorR}, ${this.#colorG}, ${this.#colorB}, ${drop.opacity * drop.depth})`;
118
+ ctx.lineWidth = lineWidth;
119
+ ctx.stroke();
120
+ }
121
+
122
+ // Splashes
123
+ for (const splash of this.#splashes) {
124
+ const px = splash.x * width;
125
+ const py = splash.y * height;
126
+
127
+ ctx.beginPath();
128
+ ctx.arc(px, py, splash.size * this.#scale, 0, Math.PI * 2);
129
+ ctx.fillStyle = `rgba(${this.#colorR}, ${this.#colorG}, ${this.#colorB}, ${splash.alpha})`;
130
+ ctx.fill();
131
+ }
132
+ }
133
+
134
+ #createDrop(initialSpread: boolean): Raindrop {
135
+ const depth = 0.3 + MULBERRY.next() * 0.7;
136
+ const fallSpeed = (3.5 + MULBERRY.next() * 5) * depth * this.#speed;
137
+
138
+ // Extend spawn range upstream of wind direction so the windward edge stays covered.
139
+ // Drops blowing right need to also spawn left of the canvas (negative x), and vice versa.
140
+ const windOffset = Math.abs(this.#wind) * 0.5;
141
+ const xMin = this.#wind > 0 ? -windOffset : 0;
142
+ const xMax = this.#wind < 0 ? 1 + windOffset : 1;
143
+
144
+ return {
145
+ x: xMin + MULBERRY.next() * (xMax - xMin),
146
+ y: initialSpread ? MULBERRY.next() * this.#groundLevel : -MULBERRY.next() * 0.1,
147
+ vx: this.#wind * fallSpeed * 0.6,
148
+ vy: fallSpeed,
149
+ length: (8 + MULBERRY.next() * 15) * this.#scale,
150
+ speed: fallSpeed,
151
+ depth,
152
+ opacity: 0.3 + MULBERRY.next() * 0.4
153
+ };
154
+ }
155
+
156
+ #createSplashBurst(x: number, y: number): void {
157
+ const count = 2 + Math.floor(MULBERRY.next() * 2);
158
+
159
+ for (let i = 0; i < count; i++) {
160
+ this.#splashes.push({
161
+ x,
162
+ y,
163
+ vx: (MULBERRY.next() - 0.5) * 2,
164
+ vy: -(1 + MULBERRY.next() * 2),
165
+ alpha: 0.5 + MULBERRY.next() * 0.3,
166
+ size: 1 + MULBERRY.next() * 2,
167
+ gravity: 0.15
168
+ });
169
+ }
170
+ }
171
+
172
+ }
@@ -0,0 +1,132 @@
1
+ import type { Point } from '../point';
2
+
3
+ export interface RaindropParticleConfig {
4
+ readonly depth?: number;
5
+ readonly groundY?: number;
6
+ readonly length?: number;
7
+ readonly scale?: number;
8
+ }
9
+
10
+ export interface SplashParticleConfig {
11
+ readonly gravity?: number;
12
+ readonly scale?: number;
13
+ readonly size?: number;
14
+ }
15
+
16
+ export class RaindropParticle {
17
+ readonly #color: [number, number, number];
18
+ readonly #depth: number;
19
+ readonly #length: number;
20
+ readonly #groundY: number;
21
+ readonly #scale: number;
22
+ readonly #opacity: number;
23
+ readonly #vx: number;
24
+ readonly #vy: number;
25
+ #x: number;
26
+ #y: number;
27
+
28
+ get isDead(): boolean {
29
+ return this.#y >= this.#groundY;
30
+ }
31
+
32
+ get position(): Point {
33
+ return {x: this.#x, y: this.#y};
34
+ }
35
+
36
+ constructor(position: Point, velocity: Point, color: [number, number, number], config: RaindropParticleConfig = {}) {
37
+ this.#x = position.x;
38
+ this.#y = position.y;
39
+ this.#vx = velocity.x;
40
+ this.#vy = velocity.y;
41
+ this.#color = color;
42
+ this.#depth = config.depth ?? (0.3 + Math.random() * 0.7);
43
+ this.#groundY = config.groundY ?? Number.POSITIVE_INFINITY;
44
+ this.#length = (config.length ?? (8 + Math.random() * 15)) * (config.scale ?? 1);
45
+ this.#opacity = 0.3 + Math.random() * 0.4;
46
+ this.#scale = config.scale ?? 1;
47
+ }
48
+
49
+ draw(ctx: CanvasRenderingContext2D): void {
50
+ const [r, g, b] = this.#color;
51
+ const speed = Math.sqrt(this.#vx * this.#vx + this.#vy * this.#vy);
52
+ const nx = speed > 0 ? this.#vx / speed : 0;
53
+ const ny = speed > 0 ? this.#vy / speed : -1;
54
+
55
+ ctx.beginPath();
56
+ ctx.moveTo(this.#x, this.#y);
57
+ ctx.lineTo(this.#x - nx * this.#length * this.#depth, this.#y - ny * this.#length * this.#depth);
58
+ ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${this.#opacity * this.#depth})`;
59
+ ctx.lineWidth = (0.4 + this.#depth * 1) * this.#scale;
60
+ ctx.lineCap = 'round';
61
+ ctx.stroke();
62
+ }
63
+
64
+ tick(dt: number = 1): void {
65
+ this.#x += this.#vx * dt;
66
+ this.#y += this.#vy * dt;
67
+ }
68
+ }
69
+
70
+ export class SplashParticle {
71
+ readonly #color: [number, number, number];
72
+ readonly #gravity: number;
73
+ readonly #size: number;
74
+ readonly #scale: number;
75
+ #x: number;
76
+ #y: number;
77
+ #vx: number;
78
+ #vy: number;
79
+ #alpha: number;
80
+
81
+ get isDead(): boolean {
82
+ return this.#alpha <= 0;
83
+ }
84
+
85
+ get position(): Point {
86
+ return {x: this.#x, y: this.#y};
87
+ }
88
+
89
+ constructor(position: Point, velocity: Point, color: [number, number, number], config: SplashParticleConfig = {}) {
90
+ this.#x = position.x;
91
+ this.#y = position.y;
92
+ this.#vx = velocity.x;
93
+ this.#vy = velocity.y;
94
+ this.#color = color;
95
+ this.#alpha = 0.5 + Math.random() * 0.3;
96
+ this.#gravity = config.gravity ?? 0.15;
97
+ this.#size = config.size ?? (1 + Math.random() * 2);
98
+ this.#scale = config.scale ?? 1;
99
+ }
100
+
101
+ static burst(position: Point, color: [number, number, number], config?: SplashParticleConfig): SplashParticle[] {
102
+ const count = 2 + Math.floor(Math.random() * 3);
103
+ const particles: SplashParticle[] = [];
104
+
105
+ for (let i = 0; i < count; i++) {
106
+ particles.push(new SplashParticle(
107
+ position,
108
+ {x: (Math.random() - 0.5) * 4, y: -(1 + Math.random() * 3)},
109
+ color,
110
+ config
111
+ ));
112
+ }
113
+
114
+ return particles;
115
+ }
116
+
117
+ draw(ctx: CanvasRenderingContext2D): void {
118
+ const [r, g, b] = this.#color;
119
+
120
+ ctx.beginPath();
121
+ ctx.arc(this.#x, this.#y, this.#size * this.#scale, 0, Math.PI * 2);
122
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${this.#alpha})`;
123
+ ctx.fill();
124
+ }
125
+
126
+ tick(dt: number = 1): void {
127
+ this.#x += this.#vx * dt;
128
+ this.#y += this.#vy * dt;
129
+ this.#vy += this.#gravity * dt;
130
+ this.#alpha -= 0.04 * dt;
131
+ }
132
+ }
@@ -0,0 +1,21 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { RainLayer } from './layer';
3
+ import type { RainVariant } from './types';
4
+
5
+ export interface RainSimulationConfig {
6
+ readonly variant?: RainVariant;
7
+ readonly drops?: number;
8
+ readonly wind?: number;
9
+ readonly speed?: number;
10
+ readonly splashes?: boolean;
11
+ readonly color?: string;
12
+ readonly groundLevel?: number;
13
+ readonly scale?: number;
14
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
15
+ }
16
+
17
+ export class RainSimulation extends SimulationCanvas {
18
+ constructor(canvas: HTMLCanvasElement, config: RainSimulationConfig = {}) {
19
+ super(canvas, new RainLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ export type RainVariant = 'drizzle' | 'downpour' | 'thunderstorm';
2
+
3
+ export type Raindrop = {
4
+ x: number;
5
+ y: number;
6
+ vx: number;
7
+ vy: number;
8
+ length: number;
9
+ speed: number;
10
+ depth: number;
11
+ opacity: number;
12
+ };
13
+
14
+ export type Splash = {
15
+ x: number;
16
+ y: number;
17
+ vx: number;
18
+ vy: number;
19
+ alpha: number;
20
+ size: number;
21
+ gravity: number;
22
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,4 @@
1
+ export { SandstormLayer } from './layer';
2
+ export { SandstormSimulation } from './simulation';
3
+ export type { SandstormSimulationConfig } from './simulation';
4
+ export type { SandGrain } from './types';
@@ -0,0 +1,135 @@
1
+ import { SimulationLayer } from '../layer';
2
+ import { MULBERRY } from './consts';
3
+ import type { SandstormSimulationConfig } from './simulation';
4
+ import type { SandGrain } from './types';
5
+
6
+ export class SandstormLayer extends SimulationLayer {
7
+ readonly #scale: number;
8
+ readonly #wind: number;
9
+ readonly #turbulence: number;
10
+ readonly #colorR: number;
11
+ readonly #colorG: number;
12
+ readonly #colorB: number;
13
+ readonly #hazeOpacity: number;
14
+ #maxCount: number;
15
+ #time: number = 0;
16
+ #grains: SandGrain[] = [];
17
+
18
+ constructor(config: SandstormSimulationConfig = {}) {
19
+ super();
20
+
21
+ this.#scale = config.scale ?? 1;
22
+ this.#maxCount = config.count ?? 300;
23
+ this.#wind = config.wind ?? 1;
24
+ this.#turbulence = config.turbulence ?? 1;
25
+ this.#hazeOpacity = config.hazeOpacity ?? 0.15;
26
+
27
+ const {r, g, b} = this.#parseColor(config.color ?? '#c2956b');
28
+ this.#colorR = r;
29
+ this.#colorG = g;
30
+ this.#colorB = b;
31
+
32
+ if (innerWidth < 991) {
33
+ this.#maxCount = Math.floor(this.#maxCount / 2);
34
+ }
35
+
36
+ for (let i = 0; i < this.#maxCount; ++i) {
37
+ this.#grains.push(this.#createGrain(true));
38
+ }
39
+ }
40
+
41
+ tick(dt: number, width: number, height: number): void {
42
+ this.#time += 0.02 * dt;
43
+
44
+ const gustX = Math.sin(this.#time * 0.3) * 0.5
45
+ + Math.sin(this.#time * 0.8 + 1) * 0.3
46
+ + Math.sin(this.#time * 2.1) * 0.2;
47
+
48
+ const gustY = Math.sin(this.#time * 0.5 + 2) * 0.15;
49
+
50
+ const baseWindX = (3 + gustX * 2) * this.#wind;
51
+ const baseWindY = gustY * this.#turbulence;
52
+
53
+ for (let index = 0; index < this.#grains.length; index++) {
54
+ const grain = this.#grains[index];
55
+
56
+ const turbX = Math.sin(this.#time * 3 + grain.turbulenceOffset) * this.#turbulence * 0.5;
57
+ const turbY = Math.cos(this.#time * 2.5 + grain.turbulenceOffset * 1.3) * this.#turbulence * 0.3;
58
+
59
+ grain.vx = (baseWindX + turbX) * grain.depth;
60
+ grain.vy = (baseWindY + turbY) * grain.depth + 0.3 * grain.depth;
61
+
62
+ grain.x += (grain.vx * dt) / width;
63
+ grain.y += (grain.vy * dt) / height;
64
+
65
+ if (grain.x > 1.1 || grain.x < -0.1 || grain.y > 1.1 || grain.y < -0.1) {
66
+ const recycled = this.#createGrain(false);
67
+
68
+ if (baseWindX > 0) {
69
+ recycled.x = -0.1;
70
+ } else {
71
+ recycled.x = 1.1;
72
+ }
73
+
74
+ recycled.y = MULBERRY.next();
75
+ this.#grains[index] = recycled;
76
+ }
77
+ }
78
+ }
79
+
80
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
81
+
82
+ const hazeFlicker = 0.9 + Math.sin(this.#time * 0.7) * 0.1;
83
+ ctx.fillStyle = `rgba(${this.#colorR}, ${this.#colorG}, ${this.#colorB}, ${this.#hazeOpacity * hazeFlicker})`;
84
+ ctx.fillRect(0, 0, width, height);
85
+
86
+ for (const grain of this.#grains) {
87
+ const px = grain.x * width;
88
+ const py = grain.y * height;
89
+ const size = grain.size * grain.depth * this.#scale;
90
+
91
+ if (size < 0.3) {
92
+ continue;
93
+ }
94
+
95
+ const speed = Math.sqrt(grain.vx * grain.vx + grain.vy * grain.vy);
96
+ const nx = speed > 0 ? grain.vx / speed : 1;
97
+ const ny = speed > 0 ? grain.vy / speed : 0;
98
+ const streakLength = size * 2 + speed * 3;
99
+
100
+ ctx.beginPath();
101
+ ctx.moveTo(px, py);
102
+ ctx.lineTo(px - nx * streakLength, py - ny * streakLength);
103
+ ctx.strokeStyle = `rgba(${this.#colorR}, ${this.#colorG}, ${this.#colorB}, ${grain.opacity * grain.depth})`;
104
+ ctx.lineWidth = size;
105
+ ctx.lineCap = 'round';
106
+ ctx.stroke();
107
+ }
108
+ }
109
+
110
+ #parseColor(color: string): {r: number; g: number; b: number} {
111
+ const canvas = document.createElement('canvas');
112
+ canvas.width = 1;
113
+ canvas.height = 1;
114
+ const ctx = canvas.getContext('2d')!;
115
+ ctx.fillStyle = color;
116
+ ctx.fillRect(0, 0, 1, 1);
117
+ const data = ctx.getImageData(0, 0, 1, 1).data;
118
+ return {r: data[0], g: data[1], b: data[2]};
119
+ }
120
+
121
+ #createGrain(initialSpread: boolean): SandGrain {
122
+ const depth = 0.2 + MULBERRY.next() * 0.8;
123
+
124
+ return {
125
+ x: initialSpread ? MULBERRY.next() : -0.1,
126
+ y: MULBERRY.next(),
127
+ vx: 0,
128
+ vy: 0,
129
+ size: (0.5 + MULBERRY.next() * 2) * this.#scale,
130
+ depth,
131
+ opacity: 0.3 + MULBERRY.next() * 0.5,
132
+ turbulenceOffset: MULBERRY.next() * Math.PI * 2
133
+ };
134
+ }
135
+ }
@@ -0,0 +1,18 @@
1
+ import { SimulationCanvas } from '../simulation-canvas';
2
+ import { SandstormLayer } from './layer';
3
+
4
+ export interface SandstormSimulationConfig {
5
+ readonly count?: number;
6
+ readonly wind?: number;
7
+ readonly turbulence?: number;
8
+ readonly color?: string;
9
+ readonly hazeOpacity?: number;
10
+ readonly scale?: number;
11
+ readonly canvasOptions?: CanvasRenderingContext2DSettings;
12
+ }
13
+
14
+ export class SandstormSimulation extends SimulationCanvas {
15
+ constructor(canvas: HTMLCanvasElement, config: SandstormSimulationConfig = {}) {
16
+ super(canvas, new SandstormLayer(config), 60, config.canvasOptions ?? {colorSpace: 'display-p3'});
17
+ }
18
+ }
@@ -0,0 +1,10 @@
1
+ export type SandGrain = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ size: number;
7
+ depth: number;
8
+ opacity: number;
9
+ turbulenceOffset: number;
10
+ };
@@ -0,0 +1,3 @@
1
+ export { ShootingStarSystem } from './system';
2
+ export type { ShootingStarSystemConfig } from './system';
3
+ export type { ShootingStar } from './types';