@basmilius/sparkle 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/index.d.mts +1053 -28
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +4840 -400
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +7 -2
  6. package/src/aurora/consts.ts +3 -0
  7. package/src/aurora/index.ts +10 -0
  8. package/src/aurora/layer.ts +180 -0
  9. package/src/aurora/types.ts +13 -0
  10. package/src/balloons/consts.ts +3 -0
  11. package/src/balloons/index.ts +12 -0
  12. package/src/balloons/layer.ts +169 -0
  13. package/src/balloons/particle.ts +110 -0
  14. package/src/balloons/types.ts +14 -0
  15. package/src/bubbles/consts.ts +3 -0
  16. package/src/bubbles/index.ts +10 -0
  17. package/src/bubbles/layer.ts +246 -0
  18. package/src/bubbles/types.ts +21 -0
  19. package/src/canvas.ts +32 -1
  20. package/src/color.ts +19 -0
  21. package/src/confetti/consts.ts +13 -13
  22. package/src/confetti/index.ts +20 -2
  23. package/src/confetti/layer.ts +155 -0
  24. package/src/confetti/particle.ts +106 -0
  25. package/src/confetti/shapes.ts +104 -0
  26. package/src/confetti/types.ts +4 -1
  27. package/src/distance.ts +1 -1
  28. package/src/donuts/consts.ts +19 -0
  29. package/src/donuts/donut.ts +12 -0
  30. package/src/donuts/index.ts +9 -0
  31. package/src/donuts/layer.ts +301 -0
  32. package/src/effect.ts +107 -0
  33. package/src/fade.ts +87 -0
  34. package/src/fireflies/consts.ts +3 -0
  35. package/src/fireflies/index.ts +12 -0
  36. package/src/fireflies/layer.ts +169 -0
  37. package/src/fireflies/particle.ts +124 -0
  38. package/src/fireflies/types.ts +17 -0
  39. package/src/firepit/consts.ts +3 -0
  40. package/src/firepit/index.ts +10 -0
  41. package/src/firepit/layer.ts +193 -0
  42. package/src/firepit/types.ts +20 -0
  43. package/src/fireworks/create-explosion.ts +237 -0
  44. package/src/fireworks/explosion.ts +9 -9
  45. package/src/fireworks/firework.ts +9 -8
  46. package/src/fireworks/index.ts +19 -3
  47. package/src/fireworks/layer.ts +203 -0
  48. package/src/fireworks/spark.ts +9 -9
  49. package/src/fireworks/types.ts +2 -2
  50. package/src/glitter/consts.ts +13 -0
  51. package/src/glitter/index.ts +9 -0
  52. package/src/glitter/layer.ts +181 -0
  53. package/src/glitter/types.ts +33 -0
  54. package/src/index.ts +27 -0
  55. package/src/lanterns/consts.ts +13 -0
  56. package/src/lanterns/index.ts +9 -0
  57. package/src/lanterns/layer.ts +178 -0
  58. package/src/lanterns/types.ts +22 -0
  59. package/src/layer.ts +26 -0
  60. package/src/leaves/consts.ts +16 -0
  61. package/src/leaves/index.ts +9 -0
  62. package/src/leaves/layer.ts +258 -0
  63. package/src/leaves/types.ts +25 -0
  64. package/src/lightning/consts.ts +3 -0
  65. package/src/lightning/index.ts +11 -0
  66. package/src/lightning/layer.ts +41 -0
  67. package/src/lightning/system.ts +196 -0
  68. package/src/lightning/types.ts +20 -0
  69. package/src/matrix/consts.ts +5 -0
  70. package/src/matrix/index.ts +9 -0
  71. package/src/matrix/layer.ts +154 -0
  72. package/src/matrix/types.ts +17 -0
  73. package/src/orbits/consts.ts +13 -0
  74. package/src/orbits/index.ts +9 -0
  75. package/src/orbits/layer.ts +213 -0
  76. package/src/orbits/types.ts +27 -0
  77. package/src/particles/consts.ts +3 -0
  78. package/src/particles/index.ts +10 -0
  79. package/src/particles/layer.ts +360 -0
  80. package/src/particles/types.ts +10 -0
  81. package/src/petals/consts.ts +13 -0
  82. package/src/petals/index.ts +10 -0
  83. package/src/petals/layer.ts +174 -0
  84. package/src/petals/types.ts +15 -0
  85. package/src/plasma/consts.ts +3 -0
  86. package/src/plasma/index.ts +10 -0
  87. package/src/plasma/layer.ts +107 -0
  88. package/src/plasma/types.ts +5 -0
  89. package/src/rain/consts.ts +3 -0
  90. package/src/rain/index.ts +12 -0
  91. package/src/rain/layer.ts +194 -0
  92. package/src/rain/particle.ts +132 -0
  93. package/src/rain/types.ts +22 -0
  94. package/src/sandstorm/consts.ts +3 -0
  95. package/src/sandstorm/index.ts +10 -0
  96. package/src/sandstorm/layer.ts +152 -0
  97. package/src/sandstorm/types.ts +10 -0
  98. package/src/scene.ts +201 -0
  99. package/src/shooting-stars/index.ts +3 -0
  100. package/src/shooting-stars/system.ts +151 -0
  101. package/src/shooting-stars/types.ts +11 -0
  102. package/src/simulation-canvas.ts +83 -0
  103. package/src/snow/consts.ts +2 -2
  104. package/src/snow/index.ts +9 -2
  105. package/src/snow/{simulation.ts → layer.ts} +64 -89
  106. package/src/sparklers/consts.ts +3 -0
  107. package/src/sparklers/index.ts +16 -0
  108. package/src/sparklers/layer.ts +220 -0
  109. package/src/sparklers/particle.ts +89 -0
  110. package/src/sparklers/types.ts +13 -0
  111. package/src/stars/consts.ts +3 -0
  112. package/src/stars/index.ts +10 -0
  113. package/src/stars/layer.ts +139 -0
  114. package/src/stars/types.ts +12 -0
  115. package/src/streamers/consts.ts +14 -0
  116. package/src/streamers/index.ts +10 -0
  117. package/src/streamers/layer.ts +223 -0
  118. package/src/streamers/types.ts +14 -0
  119. package/src/trail.ts +140 -0
  120. package/src/waves/consts.ts +3 -0
  121. package/src/waves/index.ts +10 -0
  122. package/src/waves/layer.ts +164 -0
  123. package/src/waves/types.ts +10 -0
  124. package/src/wormhole/consts.ts +3 -0
  125. package/src/wormhole/index.ts +10 -0
  126. package/src/wormhole/layer.ts +197 -0
  127. package/src/wormhole/types.ts +10 -0
  128. package/src/confetti/simulation.ts +0 -221
  129. package/src/fireworks/simulation.ts +0 -493
@@ -0,0 +1,139 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { ShootingStarSystem } from '../shooting-stars';
4
+ import { MULBERRY } from './consts';
5
+ import type { Star, StarMode } from './types';
6
+
7
+ export interface StarsConfig {
8
+ readonly mode?: StarMode;
9
+ readonly starCount?: number;
10
+ readonly shootingInterval?: [number, number];
11
+ readonly shootingSpeed?: number;
12
+ readonly twinkleSpeed?: number;
13
+ readonly color?: string;
14
+ readonly shootingColor?: string;
15
+ readonly trailLength?: number;
16
+ readonly scale?: number;
17
+ }
18
+
19
+ export class Stars extends Effect<StarsConfig> {
20
+ readonly #mode: StarMode;
21
+ #twinkleSpeed: number;
22
+ readonly #colorRGB: [number, number, number];
23
+ #scale: number;
24
+ readonly #shootingStarSystem: ShootingStarSystem | null;
25
+ #starCount: number;
26
+ #time: number = 0;
27
+ #stars: Star[] = [];
28
+
29
+ constructor(config: StarsConfig = {}) {
30
+ super();
31
+
32
+ this.#mode = config.mode ?? 'both';
33
+ this.#starCount = config.starCount ?? 150;
34
+ this.#twinkleSpeed = config.twinkleSpeed ?? 1;
35
+ this.#scale = config.scale ?? 1;
36
+
37
+ this.#colorRGB = hexToRGB(config.color ?? '#ffffff');
38
+
39
+ const shootingColorRGB = hexToRGB(config.shootingColor ?? '#ffffff');
40
+ const enableShooting = this.#mode === 'shooting' || this.#mode === 'both';
41
+
42
+ this.#shootingStarSystem = enableShooting
43
+ ? new ShootingStarSystem(
44
+ {
45
+ interval: config.shootingInterval ?? [120, 360],
46
+ color: shootingColorRGB,
47
+ trailLength: config.trailLength ?? 15,
48
+ trailAlphaFactor: 0.6,
49
+ speed: config.shootingSpeed ?? 1,
50
+ scale: this.#scale,
51
+ alphaMin: 0.8,
52
+ alphaRange: 0.2,
53
+ decayMin: 0.01,
54
+ decayRange: 0.015
55
+ },
56
+ () => MULBERRY.next()
57
+ )
58
+ : null;
59
+
60
+ if (this.#mode === 'sky' || this.#mode === 'both') {
61
+ for (let i = 0; i < this.#starCount; ++i) {
62
+ this.#stars.push(this.#createStar());
63
+ }
64
+ }
65
+ }
66
+
67
+ configure(config: Partial<StarsConfig>): void {
68
+ if (config.twinkleSpeed !== undefined) {
69
+ this.#twinkleSpeed = config.twinkleSpeed;
70
+ }
71
+ if (config.scale !== undefined) {
72
+ this.#scale = config.scale;
73
+ }
74
+ }
75
+
76
+ tick(dt: number, width: number, height: number): void {
77
+ this.#time += 0.02 * dt;
78
+ this.#shootingStarSystem?.tick(dt, width, height);
79
+ }
80
+
81
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
82
+ const [sr, sg, sb] = this.#colorRGB;
83
+
84
+ // Background stars
85
+ if (this.#mode === 'sky' || this.#mode === 'both') {
86
+ ctx.globalCompositeOperation = 'source-over';
87
+
88
+ for (const star of this.#stars) {
89
+ const px = star.x * width;
90
+ const py = star.y * height;
91
+ const alpha = star.brightness * (0.3 + 0.7 * (0.5 + 0.5 * Math.sin(this.#time * star.twinkleSpeed * this.#twinkleSpeed + star.twinklePhase)));
92
+ const size = star.size * this.#scale;
93
+
94
+ ctx.globalAlpha = alpha;
95
+
96
+ // Star dot
97
+ ctx.beginPath();
98
+ ctx.arc(px, py, size, 0, Math.PI * 2);
99
+ ctx.fillStyle = `rgb(${sr}, ${sg}, ${sb})`;
100
+ ctx.fill();
101
+
102
+ // Cross sparkle for larger stars
103
+ if (star.size > 1.5) {
104
+ const sparkleLength = size * 3;
105
+ const sparkleAlpha = alpha * 0.4;
106
+ ctx.globalAlpha = sparkleAlpha;
107
+ ctx.strokeStyle = `rgb(${sr}, ${sg}, ${sb})`;
108
+ ctx.lineWidth = 0.5;
109
+
110
+ ctx.beginPath();
111
+ ctx.moveTo(px - sparkleLength, py);
112
+ ctx.lineTo(px + sparkleLength, py);
113
+ ctx.stroke();
114
+
115
+ ctx.beginPath();
116
+ ctx.moveTo(px, py - sparkleLength);
117
+ ctx.lineTo(px, py + sparkleLength);
118
+ ctx.stroke();
119
+ }
120
+ }
121
+ }
122
+
123
+ // Shooting stars
124
+ this.#shootingStarSystem?.draw(ctx);
125
+
126
+ ctx.globalAlpha = 1;
127
+ }
128
+
129
+ #createStar(): Star {
130
+ return {
131
+ x: MULBERRY.next(),
132
+ y: MULBERRY.next(),
133
+ size: 0.5 + MULBERRY.next() * 2,
134
+ twinklePhase: MULBERRY.next() * Math.PI * 2,
135
+ twinkleSpeed: 0.5 + MULBERRY.next() * 2,
136
+ brightness: 0.3 + MULBERRY.next() * 0.7
137
+ };
138
+ }
139
+ }
@@ -0,0 +1,12 @@
1
+ export type { ShootingStar } from '../shooting-stars/types';
2
+
3
+ export type StarMode = 'sky' | 'shooting' | 'both';
4
+
5
+ export type Star = {
6
+ x: number;
7
+ y: number;
8
+ size: number;
9
+ twinklePhase: number;
10
+ twinkleSpeed: number;
11
+ brightness: number;
12
+ };
@@ -0,0 +1,14 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const STREAMER_COLORS: string[] = [
6
+ '#ff6b6b', // red
7
+ '#ffd93d', // yellow
8
+ '#6bcb77', // green
9
+ '#4d96ff', // blue
10
+ '#ff6bb5', // pink
11
+ '#845ec2', // purple
12
+ '#ff9671', // coral
13
+ '#00c9a7' // teal
14
+ ];
@@ -0,0 +1,10 @@
1
+ import { Streamers } from './layer';
2
+ import type { StreamersConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createStreamers(config?: StreamersConfig): Effect<StreamersConfig> {
6
+ return new Streamers(config);
7
+ }
8
+
9
+ export type { StreamersConfig };
10
+ export type { Streamer } from './types';
@@ -0,0 +1,223 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY, STREAMER_COLORS } from './consts';
3
+ import type { Streamer } from './types';
4
+
5
+ export interface StreamersConfig {
6
+ readonly count?: number;
7
+ readonly colors?: string[];
8
+ readonly speed?: number;
9
+ readonly scale?: number;
10
+ }
11
+
12
+ export class Streamers extends Effect<StreamersConfig> {
13
+ readonly #colors: string[];
14
+ readonly #scale: number;
15
+ #speed: number;
16
+ #count: number;
17
+ #streamers: Streamer[] = [];
18
+ #width: number = 960;
19
+ #height: number = 540;
20
+ #initialized: boolean = false;
21
+
22
+ constructor(config: StreamersConfig = {}) {
23
+ super();
24
+
25
+ this.#colors = config.colors ?? STREAMER_COLORS;
26
+ this.#scale = config.scale ?? 1;
27
+ this.#speed = config.speed ?? 1;
28
+ this.#count = config.count ?? 20;
29
+
30
+ if (innerWidth < 991) {
31
+ this.#count = Math.floor(this.#count / 2);
32
+ }
33
+ }
34
+
35
+ onResize(width: number, height: number): void {
36
+ this.#width = width;
37
+ this.#height = height;
38
+
39
+ if (!this.#initialized) {
40
+ this.#initialized = true;
41
+ this.#streamers = [];
42
+
43
+ for (let i = 0; i < this.#count; i++) {
44
+ this.#streamers.push(this.#createStreamer(true));
45
+ }
46
+ }
47
+ }
48
+
49
+ configure(config: Partial<StreamersConfig>): void {
50
+ if (config.speed !== undefined) {
51
+ this.#speed = config.speed;
52
+ }
53
+ }
54
+
55
+ tick(dt: number, width: number, height: number): void {
56
+ this.#width = width;
57
+ this.#height = height;
58
+
59
+ let alive = 0;
60
+
61
+ for (let i = 0; i < this.#streamers.length; i++) {
62
+ const streamer = this.#streamers[i];
63
+
64
+ streamer.y += streamer.fallSpeed * this.#speed * dt;
65
+ streamer.swayPhase += streamer.swaySpeed * dt;
66
+
67
+ const swayOffset = Math.sin(streamer.swayPhase) * streamer.swayAmplitude;
68
+ streamer.x += swayOffset * dt * 0.3;
69
+
70
+ this.#updateSegments(streamer, dt);
71
+
72
+ const tail = streamer.segments[streamer.segments.length - 1];
73
+ const tailY = tail ? tail.y : streamer.y;
74
+
75
+ if (tailY > height + 50) {
76
+ this.#streamers[alive++] = this.#createStreamer(false);
77
+ } else {
78
+ this.#streamers[alive++] = streamer;
79
+ }
80
+ }
81
+
82
+ this.#streamers.length = alive;
83
+
84
+ while (this.#streamers.length < this.#count) {
85
+ this.#streamers.push(this.#createStreamer(false));
86
+ }
87
+ }
88
+
89
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
90
+
91
+ for (const streamer of this.#streamers) {
92
+ this.#drawStreamer(ctx, streamer);
93
+ }
94
+ }
95
+
96
+ #createStreamer(initialSpread: boolean): Streamer {
97
+ const scale = this.#scale;
98
+ const segmentCount = 12 + Math.floor(MULBERRY.next() * 8);
99
+ const length = (80 + MULBERRY.next() * 120) * scale;
100
+ const width = (3 + MULBERRY.next() * 5) * scale;
101
+ const startX = MULBERRY.next() * this.#width;
102
+ const startY = initialSpread
103
+ ? MULBERRY.next() * this.#height
104
+ : -(MULBERRY.next() * 100 + length);
105
+ const depth = 0.4 + MULBERRY.next() * 0.6;
106
+ const curl = (0.3 + MULBERRY.next() * 0.7) * scale;
107
+ const fallSpeed = (1.5 + MULBERRY.next() * 2.5) * depth * scale;
108
+ const swayAmplitude = (0.3 + MULBERRY.next() * 0.6) * scale;
109
+ const swaySpeed = 0.02 + MULBERRY.next() * 0.04;
110
+ const swayPhase = MULBERRY.next() * Math.PI * 2;
111
+ const color = this.#colors[Math.floor(MULBERRY.next() * this.#colors.length)];
112
+
113
+ const segments: { x: number; y: number }[] = [];
114
+ const segmentLength = length / segmentCount;
115
+
116
+ for (let i = 0; i < segmentCount; i++) {
117
+ segments.push({
118
+ x: startX + Math.sin(i * curl * 0.5) * curl * 8,
119
+ y: startY - i * segmentLength
120
+ });
121
+ }
122
+
123
+ return {
124
+ x: startX,
125
+ y: startY,
126
+ length,
127
+ width,
128
+ segments,
129
+ fallSpeed,
130
+ swayPhase,
131
+ swaySpeed,
132
+ swayAmplitude,
133
+ color,
134
+ curl,
135
+ depth
136
+ };
137
+ }
138
+
139
+ #updateSegments(streamer: Streamer, dt: number): void {
140
+ const segments = streamer.segments;
141
+
142
+ if (segments.length === 0) {
143
+ return;
144
+ }
145
+
146
+ segments[0].x = streamer.x;
147
+ segments[0].y = streamer.y;
148
+
149
+ const segmentLength = streamer.length / segments.length;
150
+
151
+ for (let i = 1; i < segments.length; i++) {
152
+ const prev = segments[i - 1];
153
+ const curr = segments[i];
154
+
155
+ const curlOffset = Math.sin(streamer.swayPhase + i * streamer.curl * 0.8) * streamer.curl * 6;
156
+
157
+ const targetX = prev.x + curlOffset;
158
+ const targetY = prev.y - segmentLength;
159
+
160
+ const follow = 0.08 * dt;
161
+ curr.x += (targetX - curr.x) * follow;
162
+ curr.y += (targetY - curr.y) * follow;
163
+
164
+ const dx = curr.x - prev.x;
165
+ const dy = curr.y - prev.y;
166
+ const dist = Math.sqrt(dx * dx + dy * dy);
167
+
168
+ if (dist > segmentLength * 1.5) {
169
+ const nx = dx / dist;
170
+ const ny = dy / dist;
171
+ curr.x = prev.x + nx * segmentLength * 1.5;
172
+ curr.y = prev.y + ny * segmentLength * 1.5;
173
+ }
174
+ }
175
+ }
176
+
177
+ #drawStreamer(ctx: CanvasRenderingContext2D, streamer: Streamer): void {
178
+ const segments = streamer.segments;
179
+
180
+ if (segments.length < 2) {
181
+ return;
182
+ }
183
+
184
+ const alpha = 0.6 + streamer.depth * 0.4;
185
+ const maxWidth = streamer.width * streamer.depth;
186
+ const last = segments[segments.length - 1];
187
+
188
+ ctx.lineCap = 'round';
189
+ ctx.lineJoin = 'round';
190
+
191
+ ctx.beginPath();
192
+ ctx.moveTo(segments[0].x, segments[0].y);
193
+
194
+ for (let i = 1; i < segments.length - 1; i++) {
195
+ const curr = segments[i];
196
+ const next = segments[i + 1];
197
+ const midX = (curr.x + next.x) / 2;
198
+ const midY = (curr.y + next.y) / 2;
199
+
200
+ ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
201
+ }
202
+
203
+ ctx.lineTo(last.x, last.y);
204
+
205
+ const gradient = ctx.createLinearGradient(
206
+ segments[0].x, segments[0].y,
207
+ last.x, last.y
208
+ );
209
+ gradient.addColorStop(0, this.#adjustAlpha(streamer.color, alpha * 0.3));
210
+ gradient.addColorStop(0.3, this.#adjustAlpha(streamer.color, alpha));
211
+ gradient.addColorStop(0.7, this.#adjustAlpha(streamer.color, alpha));
212
+ gradient.addColorStop(1, this.#adjustAlpha(streamer.color, alpha * 0.1));
213
+
214
+ ctx.strokeStyle = gradient;
215
+ ctx.lineWidth = maxWidth;
216
+ ctx.stroke();
217
+ }
218
+
219
+ #adjustAlpha(color: string, alpha: number): string {
220
+ const clampedAlpha = Math.max(0, Math.min(1, alpha));
221
+ return color + Math.round(clampedAlpha * 255).toString(16).padStart(2, '0');
222
+ }
223
+ }
@@ -0,0 +1,14 @@
1
+ export type Streamer = {
2
+ x: number;
3
+ y: number;
4
+ length: number;
5
+ width: number;
6
+ segments: { x: number; y: number }[];
7
+ fallSpeed: number;
8
+ swayPhase: number;
9
+ swaySpeed: number;
10
+ swayAmplitude: number;
11
+ color: string;
12
+ curl: number;
13
+ depth: number;
14
+ };
package/src/trail.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { distance } from './distance';
2
+ import { Spark } from './fireworks/spark';
3
+ import type { Point } from './point';
4
+
5
+ export interface TrailConfig {
6
+ readonly acceleration?: number;
7
+ readonly brightness?: number;
8
+ readonly glow?: number;
9
+ readonly hue?: number;
10
+ readonly length?: number;
11
+ readonly speed?: number;
12
+ readonly width?: number;
13
+ }
14
+
15
+ export class Trail {
16
+ readonly #startPosition: Point;
17
+ readonly #position: Point;
18
+ readonly #angle: number;
19
+ readonly #totalDistance: number;
20
+ readonly #trail: Point[];
21
+ readonly #acceleration: number;
22
+ readonly #brightness: number;
23
+ readonly #glow: number;
24
+ readonly #hue: number;
25
+ readonly #width: number;
26
+ #speed: number;
27
+ #distanceTraveled: number = 0;
28
+ #isDone: boolean = false;
29
+ #sparkTimer: number = 0;
30
+ #pendingSparks: Spark[] = [];
31
+
32
+ get hue(): number {
33
+ return this.#hue;
34
+ }
35
+
36
+ get isDone(): boolean {
37
+ return this.#isDone;
38
+ }
39
+
40
+ get position(): Point {
41
+ return {...this.#position};
42
+ }
43
+
44
+ collectSparks(): Spark[] {
45
+ const sparks = this.#pendingSparks;
46
+ this.#pendingSparks = [];
47
+ return sparks;
48
+ }
49
+
50
+ constructor(start: Point, end: Point, config: TrailConfig = {}) {
51
+ this.#startPosition = {...start};
52
+ this.#position = {...start};
53
+ this.#angle = Math.atan2(end.y - start.y, end.x - start.x);
54
+ this.#totalDistance = distance(start, end);
55
+ this.#acceleration = config.acceleration ?? 1.05;
56
+ this.#brightness = config.brightness ?? 65;
57
+ this.#glow = config.glow ?? 10;
58
+ this.#hue = config.hue ?? Math.random() * 360;
59
+ this.#speed = config.speed ?? 1;
60
+ this.#width = config.width ?? 2;
61
+
62
+ const length = config.length ?? 6;
63
+ this.#trail = Array.from({length}, () => ({...start}));
64
+ }
65
+
66
+ draw(ctx: CanvasRenderingContext2D): void {
67
+ if (this.#isDone) {
68
+ return;
69
+ }
70
+
71
+ ctx.save();
72
+ ctx.lineCap = 'round';
73
+
74
+ for (let i = this.#trail.length - 1; i > 0; i--) {
75
+ const progress = i / this.#trail.length;
76
+ const alpha = (1 - progress) * 0.8;
77
+ const width = this.#width * (1 - progress * 0.5);
78
+
79
+ ctx.beginPath();
80
+ ctx.moveTo(this.#trail[i].x, this.#trail[i].y);
81
+ ctx.lineTo(this.#trail[i - 1].x, this.#trail[i - 1].y);
82
+ ctx.lineWidth = width;
83
+ ctx.strokeStyle = `hsla(${this.#hue}, 100%, ${this.#brightness}%, ${alpha})`;
84
+ ctx.stroke();
85
+ }
86
+
87
+ ctx.shadowBlur = this.#glow;
88
+ ctx.shadowColor = `hsl(${this.#hue}, 100%, 60%)`;
89
+
90
+ ctx.beginPath();
91
+ ctx.moveTo(this.#trail[0].x, this.#trail[0].y);
92
+ ctx.lineTo(this.#position.x, this.#position.y);
93
+ ctx.lineWidth = this.#width;
94
+ ctx.strokeStyle = `hsl(${this.#hue}, 100%, ${this.#brightness}%)`;
95
+ ctx.stroke();
96
+
97
+ ctx.shadowBlur = this.#glow * 1.5;
98
+ ctx.shadowColor = `hsl(${this.#hue}, 80%, 80%)`;
99
+ ctx.beginPath();
100
+ ctx.arc(this.#position.x, this.#position.y, this.#width * 0.6, 0, Math.PI * 2);
101
+ ctx.fillStyle = `hsl(${this.#hue}, 20%, 92%)`;
102
+ ctx.fill();
103
+
104
+ ctx.restore();
105
+ }
106
+
107
+ tick(dt: number = 1): void {
108
+ if (this.#isDone) {
109
+ return;
110
+ }
111
+
112
+ this.#trail.pop();
113
+ this.#trail.unshift({...this.#position});
114
+
115
+ this.#speed *= Math.pow(this.#acceleration, dt);
116
+
117
+ const vx = Math.cos(this.#angle) * this.#speed;
118
+ const vy = Math.sin(this.#angle) * this.#speed;
119
+
120
+ this.#distanceTraveled = distance(this.#startPosition, {
121
+ x: this.#position.x + vx * dt,
122
+ y: this.#position.y + vy * dt
123
+ });
124
+
125
+ if (this.#distanceTraveled >= this.#totalDistance) {
126
+ this.#isDone = true;
127
+ return;
128
+ }
129
+
130
+ this.#position.x += vx * dt;
131
+ this.#position.y += vy * dt;
132
+
133
+ this.#sparkTimer += dt;
134
+
135
+ if (this.#sparkTimer >= 3) {
136
+ this.#sparkTimer -= 3;
137
+ this.#pendingSparks.push(new Spark(this.#position, this.#hue, -vx * 0.1, -vy * 0.1));
138
+ }
139
+ }
140
+ }
@@ -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 { Waves } from './layer';
2
+ import type { WavesConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createWaves(config?: WavesConfig): Effect<WavesConfig> {
6
+ return new Waves(config);
7
+ }
8
+
9
+ export type { WavesConfig };
10
+ export type { Wave } from './types';