@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,246 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { Butterfly } from './types';
5
+
6
+ export interface ButterfliesConfig {
7
+ readonly colors?: string[];
8
+ readonly count?: number;
9
+ readonly scale?: number;
10
+ readonly size?: number;
11
+ readonly speed?: number;
12
+ }
13
+
14
+ const DEFAULT_COLORS = [
15
+ '#f4a261',
16
+ '#e76f51',
17
+ '#e9c46a',
18
+ '#2a9d8f',
19
+ '#8ecae6',
20
+ '#ffb7c5',
21
+ '#c77dff',
22
+ '#f8edeb'
23
+ ];
24
+
25
+ export class Butterflies extends Effect<ButterfliesConfig> {
26
+ readonly #scale: number;
27
+ #speed: number;
28
+ #count: number;
29
+ #size: number;
30
+ #time: number = 0;
31
+ #butterflies: Butterfly[] = [];
32
+
33
+ constructor(config: ButterfliesConfig = {}) {
34
+ super();
35
+
36
+ this.#scale = config.scale ?? 1;
37
+ this.#speed = config.speed ?? 1;
38
+ this.#count = config.count ?? 12;
39
+ this.#size = (config.size ?? 20) * this.#scale;
40
+
41
+ const colors = config.colors ?? DEFAULT_COLORS;
42
+
43
+ if (innerWidth < 991) {
44
+ this.#count = Math.floor(this.#count / 2);
45
+ }
46
+
47
+ for (let i = 0; i < this.#count; ++i) {
48
+ this.#butterflies.push(this.#createButterfly(colors));
49
+ }
50
+ }
51
+
52
+ configure(config: Partial<ButterfliesConfig>): void {
53
+ if (config.speed !== undefined) {
54
+ this.#speed = config.speed;
55
+ }
56
+ }
57
+
58
+ tick(dt: number, _width: number, _height: number): void {
59
+ this.#time += 0.003 * this.#speed * dt;
60
+
61
+ for (const butterfly of this.#butterflies) {
62
+ const timeX = this.#time * butterfly.orbitSpeedX + butterfly.orbitOffsetX;
63
+ const timeY = this.#time * butterfly.orbitSpeedY + butterfly.orbitOffsetY;
64
+
65
+ const orbitX = Math.sin(timeX) * butterfly.orbitRadiusX;
66
+ const orbitY = Math.cos(timeY) * butterfly.orbitRadiusY;
67
+
68
+ const targetX = butterfly.driftX + orbitX;
69
+ const targetY = butterfly.driftY + orbitY;
70
+
71
+ const dx = targetX - butterfly.x;
72
+ const dy = targetY - butterfly.y;
73
+
74
+ butterfly.x += dx * 0.05 * dt;
75
+ butterfly.y += dy * 0.05 * dt;
76
+
77
+ butterfly.driftX += butterfly.driftVX * dt;
78
+ butterfly.driftY += butterfly.driftVY * dt;
79
+
80
+ if (butterfly.driftX < 0.05 || butterfly.driftX > 0.95) {
81
+ butterfly.driftVX *= -1;
82
+ }
83
+
84
+ if (butterfly.driftY < 0.05 || butterfly.driftY > 0.95) {
85
+ butterfly.driftVY *= -1;
86
+ }
87
+
88
+ const moveDx = dx * 0.05 * dt;
89
+ const moveDy = dy * 0.05 * dt;
90
+
91
+ if (Math.abs(moveDx) > 0.000001 || Math.abs(moveDy) > 0.000001) {
92
+ const targetAngle = Math.atan2(moveDy, moveDx) + Math.PI / 2;
93
+ let angleDiff = targetAngle - butterfly.angle;
94
+
95
+ if (angleDiff > Math.PI) {
96
+ angleDiff -= Math.PI * 2;
97
+ } else if (angleDiff < -Math.PI) {
98
+ angleDiff += Math.PI * 2;
99
+ }
100
+
101
+ butterfly.angle += angleDiff * 0.08 * dt;
102
+ }
103
+ }
104
+ }
105
+
106
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
107
+ const globalTime = this.#time * 1000;
108
+
109
+ for (const butterfly of this.#butterflies) {
110
+ const px = butterfly.x * width;
111
+ const py = butterfly.y * height;
112
+ const flapAngle = Math.sin(globalTime * butterfly.flapSpeed * 0.012 + butterfly.flapOffset);
113
+ const wingScale = Math.abs(flapAngle);
114
+ const isFlipped = flapAngle < 0;
115
+
116
+ const size = butterfly.size * this.#size;
117
+
118
+ ctx.globalAlpha = 0.85;
119
+
120
+ this.#drawButterfly(ctx, px, py, butterfly.angle, size, wingScale, isFlipped, butterfly.colorR, butterfly.colorG, butterfly.colorB);
121
+ }
122
+
123
+ ctx.globalAlpha = 1;
124
+ ctx.resetTransform();
125
+ }
126
+
127
+ #drawButterfly(
128
+ ctx: CanvasRenderingContext2D,
129
+ px: number,
130
+ py: number,
131
+ angle: number,
132
+ size: number,
133
+ wingScale: number,
134
+ isFlipped: boolean,
135
+ r: number,
136
+ g: number,
137
+ b: number
138
+ ): void {
139
+ const cos = Math.cos(angle);
140
+ const sin = Math.sin(angle);
141
+
142
+ ctx.save();
143
+ ctx.transform(cos, sin, -sin, cos, px, py);
144
+
145
+ const ws = Math.max(0.05, wingScale);
146
+
147
+ const wingColor = `rgba(${r}, ${g}, ${b}, 0.75)`;
148
+ const wingEdgeColor = `rgba(${r}, ${g}, ${b}, 0.4)`;
149
+ const bodyColor = `rgba(${Math.floor(r * 0.4)}, ${Math.floor(g * 0.4)}, ${Math.floor(b * 0.4)}, 0.9)`;
150
+
151
+ for (const side of [-1, 1]) {
152
+ const scaleX = side * ws;
153
+
154
+ ctx.save();
155
+ ctx.scale(scaleX, 1);
156
+
157
+ ctx.fillStyle = wingColor;
158
+ ctx.beginPath();
159
+ ctx.moveTo(0, 0);
160
+ ctx.bezierCurveTo(
161
+ size * 0.8, -size * 0.2,
162
+ size * 1.1, -size * 0.9,
163
+ size * 0.5, -size * 1.0
164
+ );
165
+ ctx.bezierCurveTo(
166
+ size * 0.1, -size * 1.1,
167
+ -size * 0.1, -size * 0.6,
168
+ 0, 0
169
+ );
170
+ ctx.fill();
171
+
172
+ ctx.fillStyle = wingEdgeColor;
173
+ ctx.beginPath();
174
+ ctx.moveTo(0, 0);
175
+ ctx.bezierCurveTo(
176
+ size * 0.9, size * 0.1,
177
+ size * 1.0, size * 0.7,
178
+ size * 0.4, size * 0.8
179
+ );
180
+ ctx.bezierCurveTo(
181
+ size * 0.0, size * 0.85,
182
+ -size * 0.1, size * 0.4,
183
+ 0, 0
184
+ );
185
+ ctx.fill();
186
+
187
+ ctx.restore();
188
+ }
189
+
190
+ ctx.fillStyle = bodyColor;
191
+ const bodyLength = size * 1.2;
192
+ ctx.beginPath();
193
+ ctx.ellipse(0, 0, size * 0.08, bodyLength * 0.5, 0, 0, Math.PI * 2);
194
+ ctx.fill();
195
+
196
+ const antennaLength = size * 0.7;
197
+ ctx.strokeStyle = bodyColor;
198
+ ctx.lineWidth = 1;
199
+
200
+ for (const antSide of [-1, 1]) {
201
+ ctx.beginPath();
202
+ ctx.moveTo(antSide * size * 0.04, -bodyLength * 0.4);
203
+ ctx.quadraticCurveTo(
204
+ antSide * size * 0.3, -bodyLength * 0.6,
205
+ antSide * size * 0.25, -bodyLength * 0.5 - antennaLength
206
+ );
207
+ ctx.stroke();
208
+
209
+ ctx.fillStyle = bodyColor;
210
+ ctx.beginPath();
211
+ ctx.arc(antSide * size * 0.25, -bodyLength * 0.5 - antennaLength, size * 0.06, 0, Math.PI * 2);
212
+ ctx.fill();
213
+ }
214
+
215
+ ctx.restore();
216
+ }
217
+
218
+ #createButterfly(colors: string[]): Butterfly {
219
+ const colorStr = colors[Math.floor(MULBERRY.next() * colors.length)];
220
+ const {r, g, b} = parseColor(colorStr);
221
+
222
+ return {
223
+ x: MULBERRY.next(),
224
+ y: MULBERRY.next(),
225
+ angle: MULBERRY.next() * Math.PI * 2 + Math.PI / 2,
226
+ speed: 0.5 + MULBERRY.next() * 0.5,
227
+ flapSpeed: 4 + MULBERRY.next() * 4,
228
+ flapOffset: MULBERRY.next() * Math.PI * 2,
229
+ size: 0.7 + MULBERRY.next() * 0.6,
230
+ color: colorStr,
231
+ colorR: r,
232
+ colorG: g,
233
+ colorB: b,
234
+ orbitRadiusX: 0.06 + MULBERRY.next() * 0.15,
235
+ orbitRadiusY: 0.05 + MULBERRY.next() * 0.12,
236
+ orbitSpeedX: 0.6 + MULBERRY.next() * 1.2,
237
+ orbitSpeedY: 0.5 + MULBERRY.next() * 1.0,
238
+ orbitOffsetX: MULBERRY.next() * Math.PI * 2,
239
+ orbitOffsetY: MULBERRY.next() * Math.PI * 2,
240
+ driftX: 0.15 + MULBERRY.next() * 0.7,
241
+ driftY: 0.15 + MULBERRY.next() * 0.7,
242
+ driftVX: (MULBERRY.next() - 0.5) * 0.0005,
243
+ driftVY: (MULBERRY.next() - 0.5) * 0.0004
244
+ };
245
+ }
246
+ }
@@ -0,0 +1,23 @@
1
+ export interface Butterfly {
2
+ x: number;
3
+ y: number;
4
+ angle: number;
5
+ speed: number;
6
+ flapSpeed: number;
7
+ flapOffset: number;
8
+ size: number;
9
+ color: string;
10
+ colorR: number;
11
+ colorG: number;
12
+ colorB: number;
13
+ orbitRadiusX: number;
14
+ orbitRadiusY: number;
15
+ orbitSpeedX: number;
16
+ orbitSpeedY: number;
17
+ orbitOffsetX: number;
18
+ orbitOffsetY: number;
19
+ driftX: number;
20
+ driftY: number;
21
+ driftVX: number;
22
+ driftVY: number;
23
+ }
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
@@ -0,0 +1,9 @@
1
+ import { Caustics } from './layer';
2
+ import type { CausticsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createCaustics(config?: CausticsConfig): Effect<CausticsConfig> {
6
+ return new Caustics(config);
7
+ }
8
+
9
+ export type { CausticsConfig };
@@ -0,0 +1,107 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+
4
+ export interface CausticsConfig {
5
+ readonly speed?: number;
6
+ readonly scale?: number;
7
+ readonly resolution?: number;
8
+ readonly intensity?: number;
9
+ readonly color?: string;
10
+ }
11
+
12
+ export class Caustics extends Effect<CausticsConfig> {
13
+ #speed: number;
14
+ readonly #scale: number;
15
+ readonly #resolution: number;
16
+ #intensity: number;
17
+ readonly #colorR: number;
18
+ readonly #colorG: number;
19
+ readonly #colorB: number;
20
+ #time: number = 0;
21
+ #offscreen: HTMLCanvasElement | null = null;
22
+ #offscreenCtx: CanvasRenderingContext2D | null = null;
23
+ #imageData: ImageData | null = null;
24
+
25
+ constructor(config: CausticsConfig = {}) {
26
+ super();
27
+
28
+ this.#speed = config.speed ?? 1;
29
+ this.#scale = config.scale ?? 1;
30
+ this.#resolution = config.resolution ?? 4;
31
+ this.#intensity = config.intensity ?? 0.7;
32
+
33
+ const [r, g, b] = hexToRGB(config.color ?? '#4488cc');
34
+ this.#colorR = r;
35
+ this.#colorG = g;
36
+ this.#colorB = b;
37
+ }
38
+
39
+ configure(config: Partial<CausticsConfig>): void {
40
+ if (config.speed !== undefined) {
41
+ this.#speed = config.speed;
42
+ }
43
+ if (config.intensity !== undefined) {
44
+ this.#intensity = config.intensity;
45
+ }
46
+ }
47
+
48
+ tick(dt: number, _width: number, _height: number): void {
49
+ this.#time += 0.015 * dt * this.#speed;
50
+ }
51
+
52
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
53
+ const resolution = this.#resolution;
54
+ const offWidth = Math.ceil(width / resolution);
55
+ const offHeight = Math.ceil(height / resolution);
56
+
57
+ if (!this.#offscreen || this.#offscreen.width !== offWidth || this.#offscreen.height !== offHeight) {
58
+ this.#offscreen = document.createElement('canvas');
59
+ this.#offscreen.width = offWidth;
60
+ this.#offscreen.height = offHeight;
61
+ this.#offscreenCtx = this.#offscreen.getContext('2d');
62
+ this.#imageData = this.#offscreenCtx!.createImageData(offWidth, offHeight);
63
+ }
64
+
65
+ const data = this.#imageData!.data;
66
+ const time = this.#time;
67
+ const scale = this.#scale;
68
+ const intensity = this.#intensity;
69
+ const colorR = this.#colorR;
70
+ const colorG = this.#colorG;
71
+ const colorB = this.#colorB;
72
+
73
+ const freq1 = 40 * scale;
74
+ const freq2 = 30 * scale;
75
+ const freq3 = 35 * scale;
76
+ const freq4 = 25 * scale;
77
+ const freq5 = 45 * scale;
78
+ const amplitude = 2.5;
79
+
80
+ for (let py = 0; py < offHeight; py++) {
81
+ const worldY = py * resolution;
82
+
83
+ for (let px = 0; px < offWidth; px++) {
84
+ const worldX = px * resolution;
85
+
86
+ const v1 = Math.sin(worldX / freq1 + Math.sin(worldY / freq2 + time) * amplitude);
87
+ const v2 = Math.sin(worldY / freq3 + Math.sin(worldX / freq4 + time * 0.7) * amplitude);
88
+ const v3 = Math.sin((worldX + worldY) / freq5 + time * 0.5);
89
+
90
+ const caustic = (v1 + v2 + v3) / 3;
91
+ const clamped = Math.max(0, caustic);
92
+ const brightness = Math.pow(clamped, 2) * intensity;
93
+
94
+ const offset = (py * offWidth + px) * 4;
95
+ data[offset] = Math.min(255, colorR * brightness + (1 - brightness) * colorR * 0.15);
96
+ data[offset + 1] = Math.min(255, colorG * brightness + (1 - brightness) * colorG * 0.15);
97
+ data[offset + 2] = Math.min(255, colorB * brightness + (1 - brightness) * colorB * 0.15);
98
+ data[offset + 3] = 255;
99
+ }
100
+ }
101
+
102
+ this.#offscreenCtx!.putImageData(this.#imageData!, 0, 0);
103
+
104
+ ctx.imageSmoothingEnabled = true;
105
+ ctx.drawImage(this.#offscreen!, 0, 0, width, height);
106
+ }
107
+ }
@@ -0,0 +1,3 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(17);
@@ -0,0 +1,9 @@
1
+ import { Clouds } from './layer';
2
+ import type { CloudsConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createClouds(config?: CloudsConfig): Effect<CloudsConfig> {
6
+ return new Clouds(config);
7
+ }
8
+
9
+ export type { CloudsConfig };
@@ -0,0 +1,167 @@
1
+ import { parseColor } from '../color';
2
+ import { Effect } from '../effect';
3
+ import { MULBERRY } from './consts';
4
+ import type { Cloud } from './types';
5
+
6
+ export interface CloudsConfig {
7
+ readonly color?: string;
8
+ readonly count?: number;
9
+ readonly opacity?: number;
10
+ readonly scale?: number;
11
+ readonly speed?: number;
12
+ }
13
+
14
+ const SPRITE_COUNT = 6;
15
+ const SPRITE_SIZE = 256;
16
+
17
+ export class Clouds extends Effect<CloudsConfig> {
18
+ readonly #scale: number;
19
+ #speed: number;
20
+ #count: number;
21
+ #opacity: number;
22
+ #clouds: Cloud[] = [];
23
+ #sprites: HTMLCanvasElement[] = [];
24
+ #colorR: number = 255;
25
+ #colorG: number = 255;
26
+ #colorB: number = 255;
27
+
28
+ constructor(config: CloudsConfig = {}) {
29
+ super();
30
+
31
+ this.#scale = config.scale ?? 1;
32
+ this.#speed = config.speed ?? 0.3;
33
+ this.#count = config.count ?? 8;
34
+ this.#opacity = config.opacity ?? 0.8;
35
+
36
+ const {r, g, b} = parseColor(config.color ?? '#ffffff');
37
+ this.#colorR = r;
38
+ this.#colorG = g;
39
+ this.#colorB = b;
40
+
41
+ if (innerWidth < 991) {
42
+ this.#count = Math.max(3, Math.floor(this.#count / 2));
43
+ }
44
+
45
+ this.#sprites = this.#createSprites(r, g, b);
46
+
47
+ for (let i = 0; i < this.#count; ++i) {
48
+ this.#clouds.push(this.#createCloud(true));
49
+ }
50
+ }
51
+
52
+ configure(config: Partial<CloudsConfig>): void {
53
+ if (config.speed !== undefined) {
54
+ this.#speed = config.speed;
55
+ }
56
+
57
+ if (config.opacity !== undefined) {
58
+ this.#opacity = config.opacity;
59
+ }
60
+
61
+ if (config.color !== undefined) {
62
+ const {r, g, b} = parseColor(config.color);
63
+ this.#colorR = r;
64
+ this.#colorG = g;
65
+ this.#colorB = b;
66
+ this.#sprites = this.#createSprites(r, g, b);
67
+ }
68
+ }
69
+
70
+ tick(dt: number, _width: number, _height: number): void {
71
+ for (const cloud of this.#clouds) {
72
+ const layerSpeed = this.#speed * (0.5 + cloud.layer * 0.5);
73
+ cloud.x += layerSpeed * 0.002 * dt;
74
+
75
+ if (cloud.x > 1.4) {
76
+ cloud.x = -0.4;
77
+ }
78
+ }
79
+ }
80
+
81
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
82
+ const sortedClouds = [...this.#clouds].sort((a, b) => a.layer - b.layer);
83
+
84
+ for (const cloud of sortedClouds) {
85
+ const px = cloud.x * width;
86
+ const py = cloud.y * height;
87
+ const cloudWidth = SPRITE_SIZE * cloud.scale * this.#scale * (width / 800);
88
+ const cloudHeight = (SPRITE_SIZE * 0.45) * cloud.scale * this.#scale * (width / 800);
89
+
90
+ const depthAlpha = 0.4 + cloud.layer * 0.3;
91
+ ctx.globalAlpha = this.#opacity * depthAlpha * cloud.opacity;
92
+
93
+ ctx.drawImage(
94
+ this.#sprites[cloud.spriteIndex],
95
+ px - cloudWidth * 0.5,
96
+ py - cloudHeight * 0.5,
97
+ cloudWidth,
98
+ cloudHeight
99
+ );
100
+ }
101
+
102
+ ctx.globalAlpha = 1;
103
+ ctx.resetTransform();
104
+ }
105
+
106
+ #createSprites(r: number, g: number, b: number): HTMLCanvasElement[] {
107
+ const sprites: HTMLCanvasElement[] = [];
108
+
109
+ for (let variant = 0; variant < SPRITE_COUNT; variant++) {
110
+ const canvas = document.createElement('canvas');
111
+ canvas.width = SPRITE_SIZE;
112
+ canvas.height = Math.floor(SPRITE_SIZE * 0.5);
113
+ const spriteCtx = canvas.getContext('2d')!;
114
+
115
+ const blobCount = 3 + Math.floor(MULBERRY.next() * 3);
116
+ const cx = SPRITE_SIZE * 0.5;
117
+ const cy = SPRITE_SIZE * 0.2;
118
+
119
+ const blobs: Array<{bx: number; by: number; br: number}> = [];
120
+
121
+ blobs.push({bx: cx, by: cy, br: SPRITE_SIZE * (0.18 + MULBERRY.next() * 0.08)});
122
+
123
+ for (let i = 1; i < blobCount; i++) {
124
+ const spread = SPRITE_SIZE * 0.28;
125
+ blobs.push({
126
+ bx: cx + (MULBERRY.next() - 0.5) * spread * 2,
127
+ by: cy + (MULBERRY.next() - 0.3) * spread * 0.5,
128
+ br: SPRITE_SIZE * (0.1 + MULBERRY.next() * 0.12)
129
+ });
130
+ }
131
+
132
+ for (const blob of blobs) {
133
+ const gradient = spriteCtx.createRadialGradient(
134
+ blob.bx, blob.by, 0,
135
+ blob.bx, blob.by, blob.br
136
+ );
137
+ gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.9)`);
138
+ gradient.addColorStop(0.35, `rgba(${r}, ${g}, ${b}, 0.7)`);
139
+ gradient.addColorStop(0.7, `rgba(${r}, ${g}, ${b}, 0.25)`);
140
+ gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
141
+
142
+ spriteCtx.fillStyle = gradient;
143
+ spriteCtx.beginPath();
144
+ spriteCtx.arc(blob.bx, blob.by, blob.br, 0, Math.PI * 2);
145
+ spriteCtx.fill();
146
+ }
147
+
148
+ sprites.push(canvas);
149
+ }
150
+
151
+ return sprites;
152
+ }
153
+
154
+ #createCloud(initialSpread: boolean): Cloud {
155
+ const layer = Math.floor(MULBERRY.next() * 3);
156
+
157
+ return {
158
+ x: initialSpread ? MULBERRY.next() * 1.8 - 0.4 : -0.4,
159
+ y: 0.05 + MULBERRY.next() * 0.55,
160
+ speed: 0.5 + MULBERRY.next() * 0.5,
161
+ layer,
162
+ scale: 0.6 + MULBERRY.next() * 0.8 + layer * 0.3,
163
+ opacity: 0.6 + MULBERRY.next() * 0.4,
164
+ spriteIndex: Math.floor(MULBERRY.next() * SPRITE_COUNT)
165
+ };
166
+ }
167
+ }
@@ -0,0 +1,9 @@
1
+ export interface Cloud {
2
+ x: number;
3
+ y: number;
4
+ speed: number;
5
+ layer: number;
6
+ scale: number;
7
+ opacity: number;
8
+ spriteIndex: number;
9
+ }
@@ -89,7 +89,8 @@ export class Confetti extends Effect<ConfettiConfig> {
89
89
  const flipCos = Math.cos(p.flipAngle);
90
90
  const size = p.size;
91
91
 
92
- ctx.setTransform(
92
+ ctx.save();
93
+ ctx.transform(
93
94
  p.rotCos * flipCos * size,
94
95
  p.rotSin * flipCos * size,
95
96
  -p.rotSin * size,
@@ -100,9 +101,9 @@ export class Confetti extends Effect<ConfettiConfig> {
100
101
  ctx.globalAlpha = 1 - p.tick / p.totalTicks;
101
102
  ctx.fillStyle = p.colorStr;
102
103
  ctx.fill(SHAPE_PATHS[p.shape]);
104
+ ctx.restore();
103
105
  }
104
106
 
105
- ctx.resetTransform();
106
107
  ctx.globalAlpha = 1;
107
108
  }
108
109
 
@@ -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 { Constellation } from './layer';
2
+ import type { ConstellationConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createConstellation(config?: ConstellationConfig): Effect<ConstellationConfig> {
6
+ return new Constellation(config);
7
+ }
8
+
9
+ export type { ConstellationConfig };
10
+ export type { ConstellationStar } from './types';