@basmilius/sparkle 2.4.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,13 @@
1
+ export type PopcornKernel = {
2
+ x: number;
3
+ y: number;
4
+ vx: number;
5
+ vy: number;
6
+ size: number;
7
+ rotation: number;
8
+ rotationSpeed: number;
9
+ popped: boolean;
10
+ opacity: number;
11
+ settled: boolean;
12
+ bounces: number;
13
+ };
@@ -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 { Portal } from './layer';
2
+ import type { PortalConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createPortal(config?: PortalConfig): Effect<PortalConfig> {
6
+ return new Portal(config);
7
+ }
8
+
9
+ export type { PortalConfig };
10
+ export type { PortalDirection, PortalParticle } from './types';
@@ -0,0 +1,251 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { PortalDirection, PortalParticle } from './types';
5
+
6
+ export interface PortalConfig {
7
+ readonly speed?: number;
8
+ readonly particles?: number;
9
+ readonly size?: number;
10
+ readonly color?: string;
11
+ readonly secondaryColor?: string;
12
+ readonly direction?: PortalDirection;
13
+ readonly scale?: number;
14
+ }
15
+
16
+ export class Portal extends Effect<PortalConfig> {
17
+ #speed: number;
18
+ #scale: number;
19
+ readonly #size: number;
20
+ readonly #colorRGB: [number, number, number];
21
+ readonly #secondaryRGB: [number, number, number];
22
+ readonly #direction: PortalDirection;
23
+ #count: number;
24
+ #particles: PortalParticle[] = [];
25
+ #width: number = 960;
26
+ #height: number = 540;
27
+ #time: number = 0;
28
+ #initialized: boolean = false;
29
+
30
+ constructor(config: PortalConfig = {}) {
31
+ super();
32
+
33
+ let count = config.particles ?? 100;
34
+
35
+ this.#speed = config.speed ?? 1;
36
+ this.#scale = config.scale ?? 1;
37
+ this.#size = config.size ?? 0.3;
38
+ this.#colorRGB = hexToRGB(config.color ?? '#8844ff');
39
+ this.#secondaryRGB = hexToRGB(config.secondaryColor ?? '#44aaff');
40
+ this.#direction = config.direction ?? 'inward';
41
+
42
+ if (innerWidth < 991) {
43
+ count = Math.floor(count / 2);
44
+ }
45
+
46
+ this.#count = count;
47
+ }
48
+
49
+ onResize(width: number, height: number): void {
50
+ this.#width = width;
51
+ this.#height = height;
52
+
53
+ if (!this.#initialized) {
54
+ this.#initialized = true;
55
+ this.#particles = [];
56
+
57
+ for (let i = 0; i < this.#count; ++i) {
58
+ this.#particles.push(this.#createParticle(true));
59
+ }
60
+ }
61
+ }
62
+
63
+ configure(config: Partial<PortalConfig>): void {
64
+ if (config.speed !== undefined) {
65
+ this.#speed = config.speed;
66
+ }
67
+ if (config.scale !== undefined) {
68
+ this.#scale = config.scale;
69
+ }
70
+ }
71
+
72
+ tick(dt: number, width: number, height: number): void {
73
+ this.#width = width;
74
+ this.#height = height;
75
+ this.#time += dt * this.#speed * 0.01;
76
+
77
+ const portalRadius = Math.min(width, height) * this.#size * this.#scale;
78
+ const maxDistance = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2);
79
+
80
+ let alive = 0;
81
+
82
+ for (let i = 0; i < this.#particles.length; ++i) {
83
+ const particle = this.#particles[i];
84
+
85
+ particle.angle += particle.rotationSpeed * this.#speed * dt * this.#scale;
86
+
87
+ if (this.#direction === 'inward') {
88
+ const normalizedDistance = particle.distance / maxDistance;
89
+ const acceleration = 1 + (1 - normalizedDistance) * 2;
90
+ particle.distance -= particle.speed * this.#speed * acceleration * dt * this.#scale;
91
+
92
+ particle.opacity = Math.min(1, normalizedDistance * 2);
93
+
94
+ if (particle.distance > portalRadius * 0.1) {
95
+ this.#particles[alive++] = particle;
96
+ } else {
97
+ this.#particles[alive++] = this.#createParticle(false);
98
+ }
99
+ } else {
100
+ const normalizedDistance = particle.distance / maxDistance;
101
+ const acceleration = 1 + normalizedDistance * 2;
102
+ particle.distance += particle.speed * this.#speed * acceleration * dt * this.#scale;
103
+
104
+ particle.opacity = Math.min(1, (1 - normalizedDistance) * 2);
105
+
106
+ if (particle.distance < maxDistance + 20) {
107
+ this.#particles[alive++] = particle;
108
+ } else {
109
+ this.#particles[alive++] = this.#createParticle(false);
110
+ }
111
+ }
112
+ }
113
+
114
+ this.#particles.length = alive;
115
+ }
116
+
117
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
118
+ const cx = width / 2;
119
+ const cy = height / 2;
120
+ const portalRadius = Math.min(width, height) * this.#size * this.#scale;
121
+ const [pr, pg, pb] = this.#colorRGB;
122
+ const [sr, sg, sb] = this.#secondaryRGB;
123
+
124
+ ctx.globalCompositeOperation = 'lighter';
125
+
126
+ // Inner glow
127
+ const innerGlow = ctx.createRadialGradient(cx, cy, 0, cx, cy, portalRadius * 0.5);
128
+ innerGlow.addColorStop(0, `rgba(255, 255, 255, 0.15)`);
129
+ innerGlow.addColorStop(0.3, `rgba(${pr}, ${pg}, ${pb}, 0.1)`);
130
+ innerGlow.addColorStop(1, `rgba(${pr}, ${pg}, ${pb}, 0)`);
131
+
132
+ ctx.beginPath();
133
+ ctx.arc(cx, cy, portalRadius * 0.5, 0, Math.PI * 2);
134
+ ctx.fillStyle = innerGlow;
135
+ ctx.fill();
136
+
137
+ // Ring with distortion and swirl
138
+ const ringSegments = 6;
139
+ const time = this.#time;
140
+
141
+ for (let ring = 0; ring < ringSegments; ++ring) {
142
+ const ringPhase = (ring / ringSegments) * Math.PI * 2;
143
+ const ringAlpha = 0.15 - ring * 0.015;
144
+ const ringWidth = 3 + ring * 1.5;
145
+
146
+ ctx.beginPath();
147
+ ctx.lineWidth = ringWidth * this.#scale;
148
+
149
+ for (let angle = 0; angle <= Math.PI * 2; angle += 0.02) {
150
+ const distortion = 1
151
+ + Math.sin(angle * 3 + time * 2 + ringPhase) * 0.04
152
+ + Math.sin(angle * 5 + time * 3.7) * 0.02
153
+ + Math.sin(angle * 7 + time * 1.3 + ringPhase) * 0.015;
154
+
155
+ const radius = portalRadius * distortion;
156
+ const px = cx + Math.cos(angle + time * 0.5 + ringPhase * 0.3) * radius;
157
+ const py = cy + Math.sin(angle + time * 0.5 + ringPhase * 0.3) * radius;
158
+
159
+ if (angle === 0) {
160
+ ctx.moveTo(px, py);
161
+ } else {
162
+ ctx.lineTo(px, py);
163
+ }
164
+ }
165
+
166
+ ctx.closePath();
167
+
168
+ const mixFactor = ring / ringSegments;
169
+ const colorR = Math.round(pr + (sr - pr) * mixFactor);
170
+ const colorG = Math.round(pg + (sg - pg) * mixFactor);
171
+ const colorB = Math.round(pb + (sb - pb) * mixFactor);
172
+
173
+ ctx.strokeStyle = `rgba(${colorR}, ${colorG}, ${colorB}, ${ringAlpha})`;
174
+ ctx.stroke();
175
+ }
176
+
177
+ // Ring glow
178
+ const ringGlow = ctx.createRadialGradient(cx, cy, portalRadius * 0.8, cx, cy, portalRadius * 1.3);
179
+ ringGlow.addColorStop(0, `rgba(${pr}, ${pg}, ${pb}, 0)`);
180
+ ringGlow.addColorStop(0.5, `rgba(${pr}, ${pg}, ${pb}, 0.08)`);
181
+ ringGlow.addColorStop(1, `rgba(${pr}, ${pg}, ${pb}, 0)`);
182
+
183
+ ctx.beginPath();
184
+ ctx.arc(cx, cy, portalRadius * 1.3, 0, Math.PI * 2);
185
+ ctx.fillStyle = ringGlow;
186
+ ctx.fill();
187
+
188
+ // Particles
189
+ for (const particle of this.#particles) {
190
+ const px = cx + Math.cos(particle.angle) * particle.distance;
191
+ const py = cy + Math.sin(particle.angle) * particle.distance;
192
+
193
+ const trailAngle = this.#direction === 'inward' ? particle.angle + Math.PI : particle.angle;
194
+ const trailLength = (8 + particle.speed * 4) * this.#scale;
195
+ const tx = px + Math.cos(trailAngle) * trailLength;
196
+ const ty = py + Math.sin(trailAngle) * trailLength;
197
+
198
+ const alpha = Math.max(0.05, Math.min(1, particle.opacity));
199
+ const lineWidth = Math.max(0.5, particle.size * this.#scale);
200
+
201
+ const gradient = ctx.createLinearGradient(px, py, tx, ty);
202
+ gradient.addColorStop(0, `rgba(${sr}, ${sg}, ${sb}, ${alpha})`);
203
+ gradient.addColorStop(1, `rgba(${sr}, ${sg}, ${sb}, 0)`);
204
+
205
+ ctx.globalAlpha = 1;
206
+ ctx.beginPath();
207
+ ctx.moveTo(px, py);
208
+ ctx.lineTo(tx, ty);
209
+ ctx.strokeStyle = gradient;
210
+ ctx.lineWidth = lineWidth;
211
+ ctx.stroke();
212
+
213
+ // Glowing dot at particle head
214
+ ctx.globalAlpha = alpha;
215
+ ctx.beginPath();
216
+ ctx.arc(px, py, lineWidth * 0.8, 0, Math.PI * 2);
217
+ ctx.fillStyle = `rgb(${sr}, ${sg}, ${sb})`;
218
+ ctx.fill();
219
+ }
220
+
221
+ ctx.globalCompositeOperation = 'source-over';
222
+ ctx.globalAlpha = 1;
223
+ }
224
+
225
+ #createParticle(spread: boolean): PortalParticle {
226
+ const maxDistance = Math.sqrt((this.#width / 2) ** 2 + (this.#height / 2) ** 2);
227
+ const portalRadius = Math.min(this.#width, this.#height) * this.#size * this.#scale;
228
+ const angle = MULBERRY.next() * Math.PI * 2;
229
+
230
+ let distance: number;
231
+
232
+ if (this.#direction === 'inward') {
233
+ distance = spread
234
+ ? portalRadius + MULBERRY.next() * (maxDistance - portalRadius)
235
+ : maxDistance * (0.7 + MULBERRY.next() * 0.3);
236
+ } else {
237
+ distance = spread
238
+ ? portalRadius + MULBERRY.next() * (maxDistance - portalRadius)
239
+ : portalRadius * (1 + MULBERRY.next() * 0.2);
240
+ }
241
+
242
+ return {
243
+ angle,
244
+ distance,
245
+ speed: 0.3 + MULBERRY.next() * 1.2,
246
+ size: 0.8 + MULBERRY.next() * 2,
247
+ opacity: 1,
248
+ rotationSpeed: (0.002 + MULBERRY.next() * 0.006) * (MULBERRY.next() > 0.5 ? 1 : -1)
249
+ };
250
+ }
251
+ }
@@ -0,0 +1,10 @@
1
+ export type PortalDirection = 'inward' | 'outward';
2
+
3
+ export type PortalParticle = {
4
+ angle: number;
5
+ distance: number;
6
+ speed: number;
7
+ size: number;
8
+ opacity: number;
9
+ rotationSpeed: number;
10
+ };
@@ -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 { PulseGrid } from './layer';
2
+ import type { PulseGridConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createPulseGrid(config?: PulseGridConfig): Effect<PulseGridConfig> {
6
+ return new PulseGrid(config);
7
+ }
8
+
9
+ export type { PulseGridConfig };
10
+ export type { PulseWave } from './types';
@@ -0,0 +1,185 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { PulseWave } from './types';
5
+
6
+ export interface PulseGridConfig {
7
+ readonly spacing?: number;
8
+ readonly speed?: number;
9
+ readonly color?: string;
10
+ readonly dotSize?: number;
11
+ readonly waveCount?: number;
12
+ readonly waveSpeed?: number;
13
+ readonly scale?: number;
14
+ }
15
+
16
+ const DEFAULT_SPACING = 30;
17
+ const DEFAULT_DOT_SIZE = 2;
18
+ const DEFAULT_WAVE_COUNT = 3;
19
+ const DEFAULT_WAVE_SPEED = 100;
20
+ const TWO_PI = Math.PI * 2;
21
+
22
+ export class PulseGrid extends Effect<PulseGridConfig> {
23
+ readonly #scale: number;
24
+ readonly #dotSize: number;
25
+ readonly #r: number;
26
+ readonly #g: number;
27
+ readonly #b: number;
28
+ #speed: number;
29
+ #waveSpeed: number;
30
+ #spacing: number;
31
+ #waveCount: number;
32
+ #gridX: Float32Array = new Float32Array(0);
33
+ #gridY: Float32Array = new Float32Array(0);
34
+ #gridCount: number = 0;
35
+ #waves: PulseWave[] = [];
36
+ #spawnTimer: number = 0;
37
+ #width: number = 0;
38
+ #height: number = 0;
39
+
40
+ constructor(config: PulseGridConfig = {}) {
41
+ super();
42
+
43
+ this.#scale = config.scale ?? 1;
44
+ this.#spacing = (config.spacing ?? DEFAULT_SPACING) * this.#scale;
45
+ this.#speed = config.speed ?? 1;
46
+ this.#dotSize = (config.dotSize ?? DEFAULT_DOT_SIZE) * this.#scale;
47
+ this.#waveCount = config.waveCount ?? DEFAULT_WAVE_COUNT;
48
+ this.#waveSpeed = (config.waveSpeed ?? DEFAULT_WAVE_SPEED) * this.#scale;
49
+
50
+ const [r, g, b] = hexToRGB(config.color ?? '#4488ff');
51
+ this.#r = r;
52
+ this.#g = g;
53
+ this.#b = b;
54
+ }
55
+
56
+ configure(config: Partial<PulseGridConfig>): void {
57
+ if (config.speed !== undefined) {
58
+ this.#speed = config.speed;
59
+ }
60
+ if (config.waveSpeed !== undefined) {
61
+ this.#waveSpeed = (config.waveSpeed) * this.#scale;
62
+ }
63
+ }
64
+
65
+ onResize(width: number, height: number): void {
66
+ this.#width = width;
67
+ this.#height = height;
68
+ this.#buildGrid(width, height);
69
+ }
70
+
71
+ tick(dt: number, _width: number, _height: number): void {
72
+ const dtSeconds = dt * 0.008 * this.#speed;
73
+
74
+ // Update existing waves.
75
+ let writeIndex = 0;
76
+
77
+ for (let index = 0; index < this.#waves.length; index++) {
78
+ const wave = this.#waves[index];
79
+ wave.radius += wave.speed * dtSeconds;
80
+ wave.life -= dtSeconds * 0.5;
81
+
82
+ if (wave.life > 0 && wave.radius < wave.maxRadius) {
83
+ this.#waves[writeIndex++] = wave;
84
+ }
85
+ }
86
+
87
+ this.#waves.length = writeIndex;
88
+
89
+ // Spawn new waves periodically.
90
+ this.#spawnTimer += dtSeconds;
91
+
92
+ const spawnInterval = 1.5 / this.#waveCount;
93
+
94
+ while (this.#spawnTimer >= spawnInterval && this.#waves.length < this.#waveCount * 2) {
95
+ this.#spawnTimer -= spawnInterval;
96
+ this.#spawnWave();
97
+ }
98
+ }
99
+
100
+ draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
101
+ const waveCount = this.#waves.length;
102
+
103
+ if (waveCount === 0) {
104
+ return;
105
+ }
106
+
107
+ ctx.globalCompositeOperation = 'lighter';
108
+
109
+ const dotSize = this.#dotSize;
110
+ const r = this.#r;
111
+ const g = this.#g;
112
+ const b = this.#b;
113
+
114
+ for (let index = 0; index < this.#gridCount; index++) {
115
+ const px = this.#gridX[index];
116
+ const py = this.#gridY[index];
117
+
118
+ let brightness = 0;
119
+
120
+ for (let wi = 0; wi < waveCount; wi++) {
121
+ const wave = this.#waves[wi];
122
+ const dx = px - wave.x;
123
+ const dy = py - wave.y;
124
+ const dist = Math.sqrt(dx * dx + dy * dy);
125
+ const ringDist = Math.abs(dist - wave.radius);
126
+ const ringWidth = 20 * this.#scale;
127
+
128
+ // Gaussian brightness based on distance to wave ring.
129
+ const intensity = Math.exp(-(ringDist * ringDist) / (2 * ringWidth * ringWidth));
130
+ brightness += intensity * wave.life;
131
+ }
132
+
133
+ if (brightness < 0.01) {
134
+ continue;
135
+ }
136
+
137
+ if (brightness > 1) {
138
+ brightness = 1;
139
+ }
140
+
141
+ ctx.globalAlpha = brightness;
142
+ ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
143
+ ctx.beginPath();
144
+ ctx.arc(px, py, dotSize + dotSize * brightness * 0.5, 0, TWO_PI);
145
+ ctx.fill();
146
+ }
147
+
148
+ ctx.globalCompositeOperation = 'source-over';
149
+ ctx.globalAlpha = 1;
150
+ }
151
+
152
+ #buildGrid(width: number, height: number): void {
153
+ const spacing = this.#spacing;
154
+ const cols = Math.ceil(width / spacing) + 1;
155
+ const rows = Math.ceil(height / spacing) + 1;
156
+ const total = cols * rows;
157
+
158
+ this.#gridX = new Float32Array(total);
159
+ this.#gridY = new Float32Array(total);
160
+ this.#gridCount = total;
161
+
162
+ let index = 0;
163
+
164
+ for (let row = 0; row < rows; row++) {
165
+ for (let col = 0; col < cols; col++) {
166
+ this.#gridX[index] = col * spacing;
167
+ this.#gridY[index] = row * spacing;
168
+ index++;
169
+ }
170
+ }
171
+ }
172
+
173
+ #spawnWave(): void {
174
+ const maxDim = Math.max(this.#width, this.#height);
175
+
176
+ this.#waves.push({
177
+ x: MULBERRY.next() * this.#width,
178
+ y: MULBERRY.next() * this.#height,
179
+ radius: 0,
180
+ maxRadius: maxDim * 0.8 + MULBERRY.next() * maxDim * 0.4,
181
+ speed: this.#waveSpeed * (0.8 + MULBERRY.next() * 0.4),
182
+ life: 1
183
+ });
184
+ }
185
+ }
@@ -0,0 +1,8 @@
1
+ export type PulseWave = {
2
+ x: number;
3
+ y: number;
4
+ radius: number;
5
+ maxRadius: number;
6
+ speed: number;
7
+ life: number;
8
+ };
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(61);
@@ -0,0 +1,9 @@
1
+ import { Roots } from './layer';
2
+ import type { RootsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createRoots(config?: RootsConfig): Effect<RootsConfig> {
6
+ return new Roots(config);
7
+ }
8
+
9
+ export type { RootsConfig };