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