@basmilius/sparkle 2.3.0 → 2.5.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 (126) hide show
  1. package/dist/index.d.mts +637 -1
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +6964 -2577
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/src/balloons/layer.ts +4 -3
  7. package/src/black-hole/consts.ts +3 -0
  8. package/src/black-hole/index.ts +10 -0
  9. package/src/black-hole/layer.ts +193 -0
  10. package/src/black-hole/types.ts +8 -0
  11. package/src/boids/consts.ts +8 -0
  12. package/src/boids/index.ts +9 -0
  13. package/src/boids/layer.ts +245 -0
  14. package/src/boids/types.ts +7 -0
  15. package/src/butterflies/consts.ts +3 -0
  16. package/src/butterflies/index.ts +9 -0
  17. package/src/butterflies/layer.ts +246 -0
  18. package/src/butterflies/types.ts +23 -0
  19. package/src/caustics/consts.ts +3 -0
  20. package/src/caustics/index.ts +9 -0
  21. package/src/caustics/layer.ts +107 -0
  22. package/src/clouds/consts.ts +3 -0
  23. package/src/clouds/index.ts +9 -0
  24. package/src/clouds/layer.ts +167 -0
  25. package/src/clouds/types.ts +9 -0
  26. package/src/confetti/layer.ts +3 -2
  27. package/src/constellation/consts.ts +3 -0
  28. package/src/constellation/index.ts +10 -0
  29. package/src/constellation/layer.ts +256 -0
  30. package/src/constellation/types.ts +11 -0
  31. package/src/coral-reef/consts.ts +3 -0
  32. package/src/coral-reef/index.ts +10 -0
  33. package/src/coral-reef/layer.ts +276 -0
  34. package/src/coral-reef/types.ts +31 -0
  35. package/src/crystallization/consts.ts +3 -0
  36. package/src/crystallization/index.ts +10 -0
  37. package/src/crystallization/layer.ts +318 -0
  38. package/src/crystallization/types.ts +25 -0
  39. package/src/digital-rain/consts.ts +7 -0
  40. package/src/digital-rain/index.ts +10 -0
  41. package/src/digital-rain/layer.ts +195 -0
  42. package/src/digital-rain/types.ts +10 -0
  43. package/src/donuts/layer.ts +5 -3
  44. package/src/glitch/consts.ts +3 -0
  45. package/src/glitch/index.ts +9 -0
  46. package/src/glitch/layer.ts +231 -0
  47. package/src/glitch/types.ts +28 -0
  48. package/src/gradient-flow/consts.ts +3 -0
  49. package/src/gradient-flow/index.ts +9 -0
  50. package/src/gradient-flow/layer.ts +134 -0
  51. package/src/gradient-flow/types.ts +8 -0
  52. package/src/hologram/consts.ts +5 -0
  53. package/src/hologram/index.ts +9 -0
  54. package/src/hologram/layer.ts +205 -0
  55. package/src/hologram/types.ts +20 -0
  56. package/src/hyper-space/consts.ts +3 -0
  57. package/src/hyper-space/index.ts +10 -0
  58. package/src/hyper-space/layer.ts +167 -0
  59. package/src/hyper-space/types.ts +8 -0
  60. package/src/index.ts +29 -0
  61. package/src/interference/consts.ts +9 -0
  62. package/src/interference/index.ts +9 -0
  63. package/src/interference/layer.ts +129 -0
  64. package/src/kaleidoscope/consts.ts +12 -0
  65. package/src/kaleidoscope/index.ts +9 -0
  66. package/src/kaleidoscope/layer.ts +213 -0
  67. package/src/kaleidoscope/types.ts +19 -0
  68. package/src/lanterns/layer.ts +3 -2
  69. package/src/lava/consts.ts +3 -0
  70. package/src/lava/index.ts +9 -0
  71. package/src/lava/layer.ts +152 -0
  72. package/src/lava/types.ts +13 -0
  73. package/src/leaves/layer.ts +3 -2
  74. package/src/murmuration/consts.ts +3 -0
  75. package/src/murmuration/index.ts +10 -0
  76. package/src/murmuration/layer.ts +279 -0
  77. package/src/murmuration/types.ts +7 -0
  78. package/src/nebula/consts.ts +3 -0
  79. package/src/nebula/index.ts +10 -0
  80. package/src/nebula/layer.ts +150 -0
  81. package/src/nebula/types.ts +20 -0
  82. package/src/neon/consts.ts +5 -0
  83. package/src/neon/index.ts +9 -0
  84. package/src/neon/layer.ts +213 -0
  85. package/src/neon/types.ts +18 -0
  86. package/src/petals/layer.ts +3 -2
  87. package/src/pollen/consts.ts +3 -0
  88. package/src/pollen/index.ts +10 -0
  89. package/src/pollen/layer.ts +181 -0
  90. package/src/pollen/types.ts +10 -0
  91. package/src/popcorn/consts.ts +3 -0
  92. package/src/popcorn/index.ts +10 -0
  93. package/src/popcorn/layer.ts +218 -0
  94. package/src/popcorn/types.ts +13 -0
  95. package/src/portal/consts.ts +3 -0
  96. package/src/portal/index.ts +10 -0
  97. package/src/portal/layer.ts +251 -0
  98. package/src/portal/types.ts +10 -0
  99. package/src/pulse-grid/consts.ts +3 -0
  100. package/src/pulse-grid/index.ts +10 -0
  101. package/src/pulse-grid/layer.ts +185 -0
  102. package/src/pulse-grid/types.ts +8 -0
  103. package/src/roots/consts.ts +3 -0
  104. package/src/roots/index.ts +9 -0
  105. package/src/roots/layer.ts +218 -0
  106. package/src/roots/types.ts +23 -0
  107. package/src/smoke/consts.ts +3 -0
  108. package/src/smoke/index.ts +9 -0
  109. package/src/smoke/layer.ts +182 -0
  110. package/src/smoke/types.ts +14 -0
  111. package/src/snow/layer.ts +3 -2
  112. package/src/topography/consts.ts +3 -0
  113. package/src/topography/index.ts +9 -0
  114. package/src/topography/layer.ts +141 -0
  115. package/src/tornado/consts.ts +3 -0
  116. package/src/tornado/index.ts +10 -0
  117. package/src/tornado/layer.ts +271 -0
  118. package/src/tornado/types.ts +22 -0
  119. package/src/volcano/consts.ts +3 -0
  120. package/src/volcano/index.ts +10 -0
  121. package/src/volcano/layer.ts +261 -0
  122. package/src/volcano/types.ts +10 -0
  123. package/src/voronoi/consts.ts +3 -0
  124. package/src/voronoi/index.ts +10 -0
  125. package/src/voronoi/layer.ts +197 -0
  126. package/src/voronoi/types.ts +7 -0
@@ -0,0 +1,152 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { LavaBlob } from './types';
5
+
6
+ export interface LavaConfig {
7
+ readonly count?: number;
8
+ readonly speed?: number;
9
+ readonly colors?: string[];
10
+ readonly scale?: number;
11
+ }
12
+
13
+ const DEFAULT_COLORS = ['#ff4400', '#ff8800', '#ffcc00', '#ff0066'];
14
+ const SPRITE_SIZE = 512;
15
+ const SPRITE_CENTER = SPRITE_SIZE / 2;
16
+ const SPRITE_RADIUS = SPRITE_SIZE / 2;
17
+
18
+ export class Lava extends Effect<LavaConfig> {
19
+ readonly #scale: number;
20
+ #speed: number;
21
+ #count: number;
22
+ #colors: string[];
23
+ #blobs: LavaBlob[] = [];
24
+ #sprites: HTMLCanvasElement[] = [];
25
+ #time: number = 0;
26
+
27
+ constructor(config: LavaConfig = {}) {
28
+ super();
29
+
30
+ this.#scale = config.scale ?? 1;
31
+ this.#speed = config.speed ?? 1;
32
+ this.#count = config.count ?? 12;
33
+ this.#colors = config.colors ?? [...DEFAULT_COLORS];
34
+
35
+ if (typeof innerWidth !== 'undefined' && innerWidth < 991) {
36
+ this.#count = Math.floor(this.#count * 0.7);
37
+ }
38
+
39
+ this.#sprites = this.#createSprites();
40
+
41
+ for (let index = 0; index < this.#count; index++) {
42
+ this.#blobs.push(this.#createBlob());
43
+ }
44
+ }
45
+
46
+ configure(config: Partial<LavaConfig>): void {
47
+ if (config.speed !== undefined) {
48
+ this.#speed = config.speed;
49
+ }
50
+
51
+ if (config.count !== undefined) {
52
+ this.#count = config.count;
53
+ }
54
+ }
55
+
56
+ tick(dt: number, _width: number, _height: number): void {
57
+ this.#time += 0.001 * dt * this.#speed;
58
+
59
+ for (const blob of this.#blobs) {
60
+ blob.phase += 0.008 * dt * blob.speed * this.#speed;
61
+ blob.driftPhase += 0.0015 * dt * this.#speed;
62
+
63
+ blob.y = blob.baseY + Math.sin(blob.phase) * blob.amplitude;
64
+ blob.x += Math.sin(blob.driftPhase) * blob.driftX * 0.0001 * dt;
65
+
66
+ if (blob.x < -0.15) {
67
+ blob.x = 1.15;
68
+ } else if (blob.x > 1.15) {
69
+ blob.x = -0.15;
70
+ }
71
+ }
72
+ }
73
+
74
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
75
+ ctx.globalCompositeOperation = 'screen';
76
+ ctx.filter = 'blur(20px)';
77
+
78
+ const canvasScale = Math.min(width, height) / 600;
79
+
80
+ for (const blob of this.#blobs) {
81
+ const px = blob.x * width;
82
+ const py = blob.y * height;
83
+ const displayRadius = blob.radius * this.#scale * canvasScale;
84
+ const displaySize = displayRadius * 2;
85
+
86
+ ctx.globalAlpha = 0.6 + 0.4 * Math.sin(this.#time * 3 + blob.phase);
87
+ ctx.drawImage(
88
+ this.#sprites[blob.colorIndex],
89
+ px - displayRadius,
90
+ py - displayRadius,
91
+ displaySize,
92
+ displaySize
93
+ );
94
+ }
95
+
96
+ ctx.filter = '';
97
+ ctx.globalCompositeOperation = 'source-over';
98
+ ctx.globalAlpha = 1;
99
+ ctx.resetTransform();
100
+ }
101
+
102
+ #createSprites(): HTMLCanvasElement[] {
103
+ const sprites: HTMLCanvasElement[] = [];
104
+
105
+ for (const color of this.#colors) {
106
+ const {r, g, b} = parseColor(color);
107
+ const canvas = document.createElement('canvas');
108
+ canvas.width = SPRITE_SIZE;
109
+ canvas.height = SPRITE_SIZE;
110
+ const spriteCtx = canvas.getContext('2d')!;
111
+
112
+ const gradient = spriteCtx.createRadialGradient(
113
+ SPRITE_CENTER, SPRITE_CENTER, 0,
114
+ SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS
115
+ );
116
+
117
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 1)`);
118
+ gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.8)`);
119
+ gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.4)`);
120
+ gradient.addColorStop(0.85, `rgba(${r}, ${g}, ${b}, 0.1)`);
121
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
122
+
123
+ spriteCtx.fillStyle = gradient;
124
+ spriteCtx.beginPath();
125
+ spriteCtx.arc(SPRITE_CENTER, SPRITE_CENTER, SPRITE_RADIUS, 0, Math.PI * 2);
126
+ spriteCtx.fill();
127
+
128
+ sprites.push(canvas);
129
+ }
130
+
131
+ return sprites;
132
+ }
133
+
134
+ #createBlob(): LavaBlob {
135
+ const minRadius = 80 * this.#scale;
136
+ const maxRadius = 160 * this.#scale;
137
+
138
+ return {
139
+ x: MULBERRY.next(),
140
+ y: 0.2 + MULBERRY.next() * 0.6,
141
+ baseY: 0.2 + MULBERRY.next() * 0.6,
142
+ radius: minRadius + MULBERRY.next() * (maxRadius - minRadius),
143
+ colorIndex: Math.floor(MULBERRY.next() * this.#colors.length),
144
+ speed: 0.5 + MULBERRY.next() * 1.5,
145
+ phase: MULBERRY.next() * Math.PI * 2,
146
+ amplitude: 0.05 + MULBERRY.next() * 0.2,
147
+ directionY: MULBERRY.next() > 0.5 ? 1 : -1,
148
+ driftX: (MULBERRY.next() - 0.5) * 2,
149
+ driftPhase: MULBERRY.next() * Math.PI * 2
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,13 @@
1
+ export type LavaBlob = {
2
+ x: number;
3
+ y: number;
4
+ radius: number;
5
+ colorIndex: number;
6
+ speed: number;
7
+ phase: number;
8
+ amplitude: number;
9
+ baseY: number;
10
+ directionY: number;
11
+ driftX: number;
12
+ driftPhase: number;
13
+ };
@@ -98,7 +98,8 @@ export class Leaves extends Effect<LeavesConfig> {
98
98
  const cos = Math.cos(leaf.rotation);
99
99
  const sin = Math.sin(leaf.rotation);
100
100
 
101
- ctx.setTransform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
101
+ ctx.save();
102
+ ctx.transform(cos * scaleX, sin * scaleX, -sin, cos, px, py);
102
103
  ctx.globalAlpha = 0.3 + leaf.depth * 0.7;
103
104
  ctx.drawImage(
104
105
  this.#sprites[leaf.colorIndex % this.#sprites.length],
@@ -107,9 +108,9 @@ export class Leaves extends Effect<LeavesConfig> {
107
108
  displaySize,
108
109
  displaySize
109
110
  );
111
+ ctx.restore();
110
112
  }
111
113
 
112
- ctx.resetTransform();
113
114
  ctx.globalAlpha = 1;
114
115
  }
115
116
 
@@ -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 { Murmuration } from './layer';
2
+ import type { MurmurationConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createMurmuration(config?: MurmurationConfig): Effect<MurmurationConfig> {
6
+ return new Murmuration(config);
7
+ }
8
+
9
+ export type { MurmurationConfig };
10
+ export type { MurmurationBird } from './types';
@@ -0,0 +1,279 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { MurmurationBird } from './types';
4
+
5
+ const CELL_SIZE = 50;
6
+ const MAX_SPEED = 4;
7
+ const BOUNDARY_MARGIN = 0.1;
8
+
9
+ export interface MurmurationConfig {
10
+ readonly count?: number;
11
+ readonly speed?: number;
12
+ readonly cohesion?: number;
13
+ readonly alignment?: number;
14
+ readonly separation?: number;
15
+ readonly turnRadius?: number;
16
+ readonly color?: string;
17
+ readonly scale?: number;
18
+ }
19
+
20
+ export class Murmuration extends Effect<MurmurationConfig> {
21
+ readonly #scale: number;
22
+ readonly #color: string;
23
+ #speed: number;
24
+ #cohesion: number;
25
+ #alignment: number;
26
+ #separation: number;
27
+ #turnRadius: number;
28
+ #maxCount: number;
29
+ #time: number = 0;
30
+ #birds: MurmurationBird[] = [];
31
+ #grid: Map<number, MurmurationBird[]> = new Map();
32
+ #width: number = 960;
33
+ #height: number = 540;
34
+ #gridCols: number = 0;
35
+
36
+ constructor(config: MurmurationConfig = {}) {
37
+ super();
38
+
39
+ this.#scale = config.scale ?? 1;
40
+ this.#speed = config.speed ?? 1;
41
+ this.#cohesion = config.cohesion ?? 0.5;
42
+ this.#alignment = config.alignment ?? 0.8;
43
+ this.#separation = config.separation ?? 0.4;
44
+ this.#turnRadius = config.turnRadius ?? 0.7;
45
+ this.#color = config.color ?? '#1a1a2e';
46
+ this.#maxCount = config.count ?? 300;
47
+
48
+ if (innerWidth < 991) {
49
+ this.#maxCount = Math.floor(this.#maxCount / 2);
50
+ }
51
+ }
52
+
53
+ configure(config: Partial<MurmurationConfig>): void {
54
+ if (config.speed !== undefined) {
55
+ this.#speed = config.speed;
56
+ }
57
+ if (config.cohesion !== undefined) {
58
+ this.#cohesion = config.cohesion;
59
+ }
60
+ if (config.alignment !== undefined) {
61
+ this.#alignment = config.alignment;
62
+ }
63
+ if (config.separation !== undefined) {
64
+ this.#separation = config.separation;
65
+ }
66
+ }
67
+
68
+ onResize(width: number, height: number): void {
69
+ this.#width = width;
70
+ this.#height = height;
71
+ this.#gridCols = Math.ceil(width / CELL_SIZE);
72
+
73
+ this.#birds = [];
74
+
75
+ for (let i = 0; i < this.#maxCount; ++i) {
76
+ this.#birds.push(this.#createBird());
77
+ }
78
+ }
79
+
80
+ tick(dt: number, width: number, height: number): void {
81
+ this.#width = width;
82
+ this.#height = height;
83
+ this.#gridCols = Math.ceil(width / CELL_SIZE);
84
+ this.#time += 0.001 * this.#speed * dt;
85
+
86
+ const dtFactor = dt / 16;
87
+ const speedFactor = this.#speed * dtFactor;
88
+
89
+ this.#buildGrid();
90
+
91
+ const waveX = Math.sin(this.#time * 1.7) * width * 0.3 + width * 0.5;
92
+ const waveForce = Math.sin(this.#time * 2.3) * 0.3 * this.#turnRadius;
93
+
94
+ for (const bird of this.#birds) {
95
+ let cohX = 0;
96
+ let cohY = 0;
97
+ let cohCount = 0;
98
+ let aliVx = 0;
99
+ let aliVy = 0;
100
+ let aliCount = 0;
101
+ let sepX = 0;
102
+ let sepY = 0;
103
+
104
+ const cellX = Math.floor(bird.x / CELL_SIZE);
105
+ const cellY = Math.floor(bird.y / CELL_SIZE);
106
+
107
+ for (let ox = -1; ox <= 1; ox++) {
108
+ for (let oy = -1; oy <= 1; oy++) {
109
+ const key = (cellX + ox) + (cellY + oy) * this.#gridCols;
110
+ const cell = this.#grid.get(key);
111
+
112
+ if (!cell) {
113
+ continue;
114
+ }
115
+
116
+ for (const other of cell) {
117
+ if (other === bird) {
118
+ continue;
119
+ }
120
+
121
+ const dx = other.x - bird.x;
122
+ const dy = other.y - bird.y;
123
+ const distSq = dx * dx + dy * dy;
124
+
125
+ if (distSq > CELL_SIZE * CELL_SIZE) {
126
+ continue;
127
+ }
128
+
129
+ const dist = Math.sqrt(distSq);
130
+
131
+ cohX += other.x;
132
+ cohY += other.y;
133
+ cohCount++;
134
+
135
+ aliVx += other.vx;
136
+ aliVy += other.vy;
137
+ aliCount++;
138
+
139
+ if (dist < 15) {
140
+ const repel = 1 / (dist + 0.1);
141
+ sepX -= dx * repel;
142
+ sepY -= dy * repel;
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ let ax = 0;
149
+ let ay = 0;
150
+
151
+ if (cohCount > 0) {
152
+ const centerX = cohX / cohCount;
153
+ const centerY = cohY / cohCount;
154
+ ax += (centerX - bird.x) * this.#cohesion * 0.001;
155
+ ay += (centerY - bird.y) * this.#cohesion * 0.001;
156
+ }
157
+
158
+ if (aliCount > 0) {
159
+ const avgVx = aliVx / aliCount;
160
+ const avgVy = aliVy / aliCount;
161
+ ax += (avgVx - bird.vx) * this.#alignment * 0.05;
162
+ ay += (avgVy - bird.vy) * this.#alignment * 0.05;
163
+ }
164
+
165
+ ax += sepX * this.#separation * 0.1;
166
+ ay += sepY * this.#separation * 0.1;
167
+
168
+ const waveDist = bird.x - waveX;
169
+ const waveInfluence = Math.exp(-(waveDist * waveDist) / (width * width * 0.02));
170
+ ay += waveForce * waveInfluence * speedFactor;
171
+
172
+ const marginX = width * BOUNDARY_MARGIN;
173
+ const marginY = height * BOUNDARY_MARGIN;
174
+
175
+ if (bird.x < marginX) {
176
+ ax += (marginX - bird.x) * 0.003;
177
+ } else if (bird.x > width - marginX) {
178
+ ax -= (bird.x - (width - marginX)) * 0.003;
179
+ }
180
+
181
+ if (bird.y < marginY) {
182
+ ay += (marginY - bird.y) * 0.003;
183
+ } else if (bird.y > height - marginY) {
184
+ ay -= (bird.y - (height - marginY)) * 0.003;
185
+ }
186
+
187
+ bird.vx += ax * speedFactor;
188
+ bird.vy += ay * speedFactor;
189
+
190
+ const speed = Math.sqrt(bird.vx * bird.vx + bird.vy * bird.vy);
191
+
192
+ if (speed > MAX_SPEED * this.#speed) {
193
+ const ratio = (MAX_SPEED * this.#speed) / speed;
194
+ bird.vx *= ratio;
195
+ bird.vy *= ratio;
196
+ } else if (speed < 0.5) {
197
+ const ratio = 0.5 / (speed + 0.001);
198
+ bird.vx *= ratio;
199
+ bird.vy *= ratio;
200
+ }
201
+
202
+ bird.x += bird.vx * speedFactor;
203
+ bird.y += bird.vy * speedFactor;
204
+
205
+ if (bird.x < -20) {
206
+ bird.x = width + 20;
207
+ } else if (bird.x > width + 20) {
208
+ bird.x = -20;
209
+ }
210
+
211
+ if (bird.y < -20) {
212
+ bird.y = height + 20;
213
+ } else if (bird.y > height + 20) {
214
+ bird.y = -20;
215
+ }
216
+ }
217
+ }
218
+
219
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
220
+ ctx.fillStyle = this.#color;
221
+
222
+ for (const bird of this.#birds) {
223
+ const angle = Math.atan2(bird.vy, bird.vx);
224
+ const size = bird.size * this.#scale;
225
+
226
+ const cos = Math.cos(angle);
227
+ const sin = Math.sin(angle);
228
+
229
+ const tipX = bird.x + cos * size * 2;
230
+ const tipY = bird.y + sin * size * 2;
231
+
232
+ const leftX = bird.x + (-cos * size - sin * size);
233
+ const leftY = bird.y + (-sin * size + cos * size);
234
+
235
+ const rightX = bird.x + (-cos * size + sin * size);
236
+ const rightY = bird.y + (-sin * size - cos * size);
237
+
238
+ ctx.beginPath();
239
+ ctx.moveTo(tipX, tipY);
240
+ ctx.lineTo(leftX, leftY);
241
+ ctx.lineTo(bird.x - cos * size * 0.3, bird.y - sin * size * 0.3);
242
+ ctx.lineTo(rightX, rightY);
243
+ ctx.closePath();
244
+ ctx.fill();
245
+ }
246
+ }
247
+
248
+ #buildGrid(): void {
249
+ this.#grid.clear();
250
+
251
+ for (const bird of this.#birds) {
252
+ const cellX = Math.floor(bird.x / CELL_SIZE);
253
+ const cellY = Math.floor(bird.y / CELL_SIZE);
254
+ const key = cellX + cellY * this.#gridCols;
255
+
256
+ let cell = this.#grid.get(key);
257
+
258
+ if (!cell) {
259
+ cell = [];
260
+ this.#grid.set(key, cell);
261
+ }
262
+
263
+ cell.push(bird);
264
+ }
265
+ }
266
+
267
+ #createBird(): MurmurationBird {
268
+ const angle = MULBERRY.next() * Math.PI * 2;
269
+ const speed = 1 + MULBERRY.next() * 2;
270
+
271
+ return {
272
+ x: MULBERRY.next() * this.#width,
273
+ y: MULBERRY.next() * this.#height,
274
+ vx: Math.cos(angle) * speed,
275
+ vy: Math.sin(angle) * speed,
276
+ size: 1.5 + MULBERRY.next() * 1.5
277
+ };
278
+ }
279
+ }
@@ -0,0 +1,7 @@
1
+ export type MurmurationBird = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ size: number;
7
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(19);
@@ -0,0 +1,10 @@
1
+ import { Nebula } from './layer';
2
+ import type { NebulaConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createNebula(config?: NebulaConfig): Effect<NebulaConfig> {
6
+ return new Nebula(config);
7
+ }
8
+
9
+ export type { NebulaConfig };
10
+ export type { NebulaBlob, NebulaStar } from './types';
@@ -0,0 +1,150 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { NebulaBlob, NebulaStar } from './types';
4
+
5
+ export interface NebulaConfig {
6
+ readonly starCount?: number;
7
+ readonly speed?: number;
8
+ readonly colors?: string[];
9
+ readonly scale?: number;
10
+ }
11
+
12
+ const DEFAULT_COLORS = ['#ff6b9d', '#c44dff', '#4d79ff', '#00d4ff'];
13
+
14
+ export class Nebula extends Effect<NebulaConfig> {
15
+ readonly #scale: number;
16
+ #speed: number;
17
+ #time: number = 0;
18
+ #colors: string[];
19
+ #blobs: NebulaBlob[] = [];
20
+ #stars: NebulaStar[] = [];
21
+ #maxStars: number;
22
+
23
+ constructor(config: NebulaConfig = {}) {
24
+ super();
25
+
26
+ this.#scale = config.scale ?? 1;
27
+ this.#speed = config.speed ?? 0.3;
28
+ this.#colors = config.colors ?? DEFAULT_COLORS;
29
+ this.#maxStars = config.starCount ?? 150;
30
+
31
+ if (typeof globalThis.innerWidth !== 'undefined' && globalThis.innerWidth < 991) {
32
+ this.#maxStars = Math.floor(this.#maxStars / 2);
33
+ }
34
+
35
+ this.#initBlobs();
36
+ this.#initStars();
37
+ }
38
+
39
+ configure(config: Partial<NebulaConfig>): void {
40
+ if (config.speed !== undefined) {
41
+ this.#speed = config.speed;
42
+ }
43
+
44
+ if (config.colors !== undefined) {
45
+ this.#colors = config.colors;
46
+ }
47
+ }
48
+
49
+ tick(dt: number, _width: number, _height: number): void {
50
+ this.#time += 0.001 * this.#speed * dt;
51
+ }
52
+
53
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
54
+ ctx.globalCompositeOperation = 'source-over';
55
+ ctx.globalAlpha = 1;
56
+ ctx.fillStyle = 'rgb(2, 0, 20)';
57
+ ctx.fillRect(0, 0, width, height);
58
+
59
+ ctx.globalCompositeOperation = 'screen';
60
+
61
+ for (const blob of this.#blobs) {
62
+ const cx = (blob.x + Math.sin(this.#time * blob.driftSpeedX + blob.driftOffsetX) * 0.12) * width;
63
+ const cy = (blob.y + Math.cos(this.#time * blob.driftSpeedY + blob.driftOffsetY) * 0.09) * height;
64
+ const radius = blob.radius * Math.min(width, height) * this.#scale;
65
+
66
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
67
+ const color = this.#colors[blob.colorIndex % this.#colors.length];
68
+ gradient.addColorStop(0, this.#colorWithAlpha(color, blob.opacity));
69
+ gradient.addColorStop(0.4, this.#colorWithAlpha(color, blob.opacity * 0.4));
70
+ gradient.addColorStop(1, this.#colorWithAlpha(color, 0));
71
+
72
+ ctx.globalAlpha = 1;
73
+ ctx.beginPath();
74
+ ctx.ellipse(cx, cy, radius, radius * (0.6 + blob.opacity * 0.4), this.#time * 0.05 * blob.driftSpeedX, 0, Math.PI * 2);
75
+ ctx.fillStyle = gradient;
76
+ ctx.fill();
77
+ }
78
+
79
+ ctx.globalCompositeOperation = 'source-over';
80
+
81
+ for (const star of this.#stars) {
82
+ const px = star.x * width;
83
+ const py = star.y * height;
84
+ const twinkle = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(this.#time * star.twinkleSpeed * 60 + star.twinkleOffset));
85
+ const alpha = star.brightness * twinkle;
86
+ const size = star.size * this.#scale;
87
+
88
+ ctx.globalAlpha = alpha;
89
+ ctx.fillStyle = '#ffffff';
90
+ ctx.beginPath();
91
+ ctx.arc(px, py, size, 0, Math.PI * 2);
92
+ ctx.fill();
93
+
94
+ if (star.brightness > 0.7 && size > 1) {
95
+ ctx.globalAlpha = alpha * 0.4;
96
+ ctx.beginPath();
97
+ ctx.arc(px, py, size * 2.5, 0, Math.PI * 2);
98
+ ctx.fill();
99
+ }
100
+ }
101
+
102
+ ctx.globalAlpha = 1;
103
+ ctx.resetTransform();
104
+ ctx.globalCompositeOperation = 'source-over';
105
+ }
106
+
107
+ #initBlobs(): void {
108
+ const blobCount = 8 + this.#colors.length * 2;
109
+
110
+ for (let i = 0; i < blobCount; ++i) {
111
+ this.#blobs.push({
112
+ x: 0.1 + MULBERRY.next() * 0.8,
113
+ y: 0.1 + MULBERRY.next() * 0.8,
114
+ radius: 0.3 + MULBERRY.next() * 0.3,
115
+ driftSpeedX: 0.3 + MULBERRY.next() * 0.7,
116
+ driftSpeedY: 0.2 + MULBERRY.next() * 0.6,
117
+ driftOffsetX: MULBERRY.next() * Math.PI * 2,
118
+ driftOffsetY: MULBERRY.next() * Math.PI * 2,
119
+ colorIndex: Math.floor(MULBERRY.next() * this.#colors.length),
120
+ opacity: 0.2 + MULBERRY.next() * 0.3
121
+ });
122
+ }
123
+ }
124
+
125
+ #initStars(): void {
126
+ for (let i = 0; i < this.#maxStars; ++i) {
127
+ this.#stars.push({
128
+ x: MULBERRY.next(),
129
+ y: MULBERRY.next(),
130
+ size: 0.5 + MULBERRY.next() * 1.5,
131
+ twinkleSpeed: 0.5 + MULBERRY.next() * 2,
132
+ twinkleOffset: MULBERRY.next() * Math.PI * 2,
133
+ brightness: 0.3 + MULBERRY.next() * 0.7
134
+ });
135
+ }
136
+ }
137
+
138
+ #colorWithAlpha(hex: string, alpha: number): string {
139
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
140
+
141
+ if (!result) {
142
+ return `rgba(255, 255, 255, ${alpha})`;
143
+ }
144
+
145
+ const r = parseInt(result[1], 16);
146
+ const g = parseInt(result[2], 16);
147
+ const b = parseInt(result[3], 16);
148
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
149
+ }
150
+ }
@@ -0,0 +1,20 @@
1
+ export interface NebulaBlob {
2
+ x: number;
3
+ y: number;
4
+ radius: number;
5
+ driftSpeedX: number;
6
+ driftSpeedY: number;
7
+ driftOffsetX: number;
8
+ driftOffsetY: number;
9
+ colorIndex: number;
10
+ opacity: number;
11
+ }
12
+
13
+ export interface NebulaStar {
14
+ x: number;
15
+ y: number;
16
+ size: number;
17
+ twinkleSpeed: number;
18
+ twinkleOffset: number;
19
+ brightness: number;
20
+ }
@@ -0,0 +1,5 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(47);
4
+
5
+ export const DEFAULT_COLORS = ['#ff0080', '#00ffff', '#ffff00', '#ff6600', '#aa00ff'];
@@ -0,0 +1,9 @@
1
+ import { Neon } from './layer';
2
+ import type { NeonConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createNeon(config?: NeonConfig): Effect<NeonConfig> {
6
+ return new Neon(config);
7
+ }
8
+
9
+ export type { NeonConfig };