@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,318 @@
1
+ import { Effect } from '../effect';
2
+ import { MULBERRY } from './consts';
3
+ import type { CrystalBranch, CrystalSeed } from './types';
4
+
5
+ const DEG_TO_RAD = Math.PI / 180;
6
+ const TAU = Math.PI * 2;
7
+ const MAX_BRANCHES_PER_SEED = 80;
8
+
9
+ export interface CrystallizationConfig {
10
+ readonly seeds?: number;
11
+ readonly speed?: number;
12
+ readonly branchAngle?: number;
13
+ readonly maxDepth?: number;
14
+ readonly color?: string;
15
+ readonly scale?: number;
16
+ }
17
+
18
+ const enum SeedPhase {
19
+ Growing,
20
+ Holding,
21
+ Fading
22
+ }
23
+
24
+ export class Crystallization extends Effect<CrystallizationConfig> {
25
+ readonly #scale: number;
26
+ readonly #seedCount: number;
27
+ readonly #branchAngle: number;
28
+ readonly #maxDepth: number;
29
+ #speed: number;
30
+ #seeds: CrystalSeed[] = [];
31
+ #time: number = 0;
32
+ #width: number = 0;
33
+ #height: number = 0;
34
+ #colorR: number;
35
+ #colorG: number;
36
+ #colorB: number;
37
+
38
+ constructor(config: CrystallizationConfig = {}) {
39
+ super();
40
+
41
+ this.#scale = config.scale ?? 1;
42
+ this.#seedCount = config.seeds ?? 5;
43
+ this.#speed = config.speed ?? 1;
44
+ this.#branchAngle = (config.branchAngle ?? 60) * DEG_TO_RAD;
45
+ this.#maxDepth = config.maxDepth ?? 5;
46
+
47
+ const {r, g, b} = this.#parseColor(config.color ?? '#88ccff');
48
+ this.#colorR = r;
49
+ this.#colorG = g;
50
+ this.#colorB = b;
51
+ }
52
+
53
+ configure(config: Partial<CrystallizationConfig>): void {
54
+ if (config.speed !== undefined) {
55
+ this.#speed = config.speed;
56
+ }
57
+ }
58
+
59
+ onResize(width: number, height: number): void {
60
+ this.#width = width;
61
+ this.#height = height;
62
+ this.#seeds = [];
63
+
64
+ for (let idx = 0; idx < this.#seedCount; ++idx) {
65
+ this.#seeds.push(this.#createSeed(idx * 1.5));
66
+ }
67
+ }
68
+
69
+ tick(dt: number, _width: number, _height: number): void {
70
+ this.#time += 0.02 * dt * this.#speed;
71
+ const growSpeed = 0.8 * dt * this.#speed;
72
+
73
+ for (let idx = 0; idx < this.#seeds.length; ++idx) {
74
+ const seed = this.#seeds[idx];
75
+
76
+ if (seed.delay > 0) {
77
+ seed.delay -= 0.02 * dt * this.#speed;
78
+ continue;
79
+ }
80
+
81
+ if (seed.phase === SeedPhase.Growing) {
82
+ const allGrown = this.#tickBranches(seed, seed.branches, growSpeed);
83
+
84
+ if (allGrown) {
85
+ seed.phase = SeedPhase.Holding;
86
+ seed.holdTimer = 120;
87
+ }
88
+ } else if (seed.phase === SeedPhase.Holding) {
89
+ seed.holdTimer -= dt * this.#speed;
90
+
91
+ if (seed.holdTimer <= 0) {
92
+ seed.phase = SeedPhase.Fading;
93
+ }
94
+ } else {
95
+ seed.alpha -= 0.008 * dt * this.#speed;
96
+
97
+ if (seed.alpha <= 0) {
98
+ this.#seeds[idx] = this.#createSeed(0);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ draw(ctx: CanvasRenderingContext2D, _width: number, _height: number): void {
105
+ const cr = this.#colorR;
106
+ const cg = this.#colorG;
107
+ const cb = this.#colorB;
108
+
109
+ ctx.lineCap = 'round';
110
+
111
+ for (const seed of this.#seeds) {
112
+ if (seed.delay > 0 || seed.alpha <= 0) {
113
+ continue;
114
+ }
115
+
116
+ const baseAlpha = seed.alpha;
117
+
118
+ this.#drawBranches(ctx, seed.branches, baseAlpha, cr, cg, cb);
119
+ this.#drawSparkles(ctx, seed.branches, baseAlpha, seed.sparklePhase);
120
+ }
121
+
122
+ ctx.globalAlpha = 1;
123
+ }
124
+
125
+ #tickBranches(seed: CrystalSeed, branches: CrystalBranch[], growSpeed: number): boolean {
126
+ let allGrown = true;
127
+
128
+ for (const branch of branches) {
129
+ if (branch.growing) {
130
+ branch.currentLength += growSpeed * (1 + branch.depth * 0.3);
131
+
132
+ if (branch.currentLength >= branch.targetLength) {
133
+ branch.currentLength = branch.targetLength;
134
+ branch.growing = false;
135
+ branch.grown = true;
136
+
137
+ if (branch.depth < this.#maxDepth && seed.branchCount < MAX_BRANCHES_PER_SEED) {
138
+ this.#spawnChildren(seed, branch);
139
+ }
140
+ }
141
+
142
+ allGrown = false;
143
+ }
144
+
145
+ if (branch.children.length > 0) {
146
+ if (!this.#tickBranches(seed, branch.children, growSpeed)) {
147
+ allGrown = false;
148
+ }
149
+ }
150
+
151
+ if (!branch.grown) {
152
+ allGrown = false;
153
+ }
154
+ }
155
+
156
+ return allGrown;
157
+ }
158
+
159
+ #drawBranches(ctx: CanvasRenderingContext2D, branches: CrystalBranch[], baseAlpha: number, cr: number, cg: number, cb: number): void {
160
+ for (const branch of branches) {
161
+ if (branch.currentLength <= 0) {
162
+ continue;
163
+ }
164
+
165
+ const endX = branch.x + Math.cos(branch.angle) * branch.currentLength;
166
+ const endY = branch.y + Math.sin(branch.angle) * branch.currentLength;
167
+ const depthFade = 1 - branch.depth * 0.12;
168
+
169
+ ctx.globalAlpha = baseAlpha * 0.12 * depthFade;
170
+ ctx.strokeStyle = `rgb(${cr}, ${cg}, ${cb})`;
171
+ ctx.lineWidth = branch.width + 4 * this.#scale;
172
+ ctx.beginPath();
173
+ ctx.moveTo(branch.x, branch.y);
174
+ ctx.lineTo(endX, endY);
175
+ ctx.stroke();
176
+
177
+ ctx.globalAlpha = baseAlpha * 0.8 * depthFade;
178
+ ctx.lineWidth = branch.width;
179
+ ctx.beginPath();
180
+ ctx.moveTo(branch.x, branch.y);
181
+ ctx.lineTo(endX, endY);
182
+ ctx.stroke();
183
+
184
+ if (branch.children.length > 0) {
185
+ this.#drawBranches(ctx, branch.children, baseAlpha, cr, cg, cb);
186
+ }
187
+ }
188
+ }
189
+
190
+ #drawSparkles(ctx: CanvasRenderingContext2D, branches: CrystalBranch[], baseAlpha: number, phase: number): void {
191
+ for (const branch of branches) {
192
+ if (branch.currentLength <= 0) {
193
+ continue;
194
+ }
195
+
196
+ if (branch.growing || (branch.grown && branch.children.length === 0)) {
197
+ const tipX = branch.x + Math.cos(branch.angle) * branch.currentLength;
198
+ const tipY = branch.y + Math.sin(branch.angle) * branch.currentLength;
199
+ const sparkle = 0.5 + 0.5 * Math.sin(this.#time * 10 + phase + branch.angle * 3);
200
+ const radius = (1 + sparkle * 1.5) * this.#scale;
201
+
202
+ ctx.globalAlpha = baseAlpha * sparkle * 0.8;
203
+ ctx.fillStyle = '#ffffff';
204
+ ctx.beginPath();
205
+ ctx.arc(tipX, tipY, radius, 0, TAU);
206
+ ctx.fill();
207
+ }
208
+
209
+ if (branch.children.length > 0) {
210
+ this.#drawSparkles(ctx, branch.children, baseAlpha, phase);
211
+ }
212
+ }
213
+ }
214
+
215
+ #createSeed(delay: number): CrystalSeed {
216
+ const seed: CrystalSeed = {
217
+ x: MULBERRY.next() * this.#width,
218
+ y: MULBERRY.next() * this.#height,
219
+ branches: [],
220
+ sparklePhase: MULBERRY.next() * TAU,
221
+ alpha: 1,
222
+ phase: SeedPhase.Growing,
223
+ holdTimer: 0,
224
+ delay,
225
+ branchCount: 0
226
+ };
227
+
228
+ const baseLength = (30 + MULBERRY.next() * 40) * this.#scale;
229
+
230
+ for (let branchIdx = 0; branchIdx < 6; ++branchIdx) {
231
+ const angle = (TAU / 6) * branchIdx + MULBERRY.next() * 0.1;
232
+
233
+ seed.branches.push({
234
+ x: seed.x,
235
+ y: seed.y,
236
+ angle,
237
+ length: 0,
238
+ targetLength: baseLength + MULBERRY.next() * 20 * this.#scale,
239
+ currentLength: 0,
240
+ depth: 0,
241
+ children: [],
242
+ width: (2.5 + MULBERRY.next()) * this.#scale,
243
+ growing: true,
244
+ grown: false
245
+ });
246
+
247
+ seed.branchCount += 1;
248
+ }
249
+
250
+ return seed;
251
+ }
252
+
253
+ #spawnChildren(seed: CrystalSeed, parent: CrystalBranch): void {
254
+ const tipX = parent.x + Math.cos(parent.angle) * parent.targetLength;
255
+ const tipY = parent.y + Math.sin(parent.angle) * parent.targetLength;
256
+ const childLength = parent.targetLength * (0.5 + MULBERRY.next() * 0.2);
257
+ const childWidth = Math.max(0.5 * this.#scale, parent.width * 0.65);
258
+ const nextDepth = parent.depth + 1;
259
+
260
+ const spawnChance = 1 - nextDepth * 0.15;
261
+
262
+ const angles = [
263
+ parent.angle + this.#branchAngle,
264
+ parent.angle - this.#branchAngle
265
+ ];
266
+
267
+ for (const angle of angles) {
268
+ if (MULBERRY.next() > spawnChance || seed.branchCount >= MAX_BRANCHES_PER_SEED) {
269
+ continue;
270
+ }
271
+
272
+ parent.children.push({
273
+ x: tipX,
274
+ y: tipY,
275
+ angle,
276
+ length: 0,
277
+ targetLength: childLength + MULBERRY.next() * 10 * this.#scale,
278
+ currentLength: 0,
279
+ depth: nextDepth,
280
+ children: [],
281
+ width: childWidth,
282
+ growing: true,
283
+ grown: false
284
+ });
285
+
286
+ seed.branchCount += 1;
287
+ }
288
+
289
+ if (MULBERRY.next() > 0.5 && seed.branchCount < MAX_BRANCHES_PER_SEED) {
290
+ parent.children.push({
291
+ x: tipX,
292
+ y: tipY,
293
+ angle: parent.angle + (MULBERRY.next() - 0.5) * 0.15,
294
+ length: 0,
295
+ targetLength: childLength * 0.7 + MULBERRY.next() * 8 * this.#scale,
296
+ currentLength: 0,
297
+ depth: nextDepth,
298
+ children: [],
299
+ width: childWidth,
300
+ growing: true,
301
+ grown: false
302
+ });
303
+
304
+ seed.branchCount += 1;
305
+ }
306
+ }
307
+
308
+ #parseColor(color: string): { r: number; g: number; b: number } {
309
+ const canvas = document.createElement('canvas');
310
+ canvas.width = 1;
311
+ canvas.height = 1;
312
+ const ctx = canvas.getContext('2d')!;
313
+ ctx.fillStyle = color;
314
+ ctx.fillRect(0, 0, 1, 1);
315
+ const data = ctx.getImageData(0, 0, 1, 1).data;
316
+ return {r: data[0], g: data[1], b: data[2]};
317
+ }
318
+ }
@@ -0,0 +1,25 @@
1
+ export type CrystalBranch = {
2
+ x: number;
3
+ y: number;
4
+ angle: number;
5
+ length: number;
6
+ targetLength: number;
7
+ currentLength: number;
8
+ depth: number;
9
+ children: CrystalBranch[];
10
+ width: number;
11
+ growing: boolean;
12
+ grown: boolean;
13
+ };
14
+
15
+ export type CrystalSeed = {
16
+ x: number;
17
+ y: number;
18
+ branches: CrystalBranch[];
19
+ sparklePhase: number;
20
+ alpha: number;
21
+ phase: number;
22
+ holdTimer: number;
23
+ delay: number;
24
+ branchCount: number;
25
+ };
@@ -0,0 +1,7 @@
1
+ import { type Mulberry32, mulberry32 } from '@basmilius/utils';
2
+
3
+ export const MULBERRY: Mulberry32 = mulberry32(13);
4
+
5
+ export const BINARY_CHARS = '01';
6
+ export const HEX_CHARS = '0123456789ABCDEF';
7
+ export const MIXED_CHARS = '01023456789ABCDEF';
@@ -0,0 +1,10 @@
1
+ import { DigitalRain } from './layer';
2
+ import type { DigitalRainConfig } from './layer';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createDigitalRain(config?: DigitalRainConfig): Effect<DigitalRainConfig> {
6
+ return new DigitalRain(config);
7
+ }
8
+
9
+ export type { DigitalRainConfig };
10
+ export type { DigitalRainColumn, DigitalRainMode } from './types';
@@ -0,0 +1,195 @@
1
+ import { hexToRGB } from '@basmilius/utils';
2
+ import { Effect } from '../effect';
3
+ import { BINARY_CHARS, HEX_CHARS, MIXED_CHARS, MULBERRY } from './consts';
4
+ import type { DigitalRainColumn, DigitalRainMode } from './types';
5
+
6
+ export interface DigitalRainConfig {
7
+ readonly speed?: number;
8
+ readonly fontSize?: number;
9
+ readonly columns?: number;
10
+ readonly mode?: DigitalRainMode;
11
+ readonly color?: string;
12
+ readonly trailLength?: number;
13
+ readonly scale?: number;
14
+ }
15
+
16
+ export class DigitalRain extends Effect<DigitalRainConfig> {
17
+ #speed: number;
18
+ #trailLength: number;
19
+ readonly #fontSize: number;
20
+ readonly #mode: DigitalRainMode;
21
+ readonly #colorRGB: [number, number, number];
22
+ readonly #scale: number;
23
+ #maxColumns: number;
24
+ #columns: DigitalRainColumn[] = [];
25
+ #respawnTimers: number[] = [];
26
+ #width: number = 960;
27
+ #height: number = 540;
28
+ #initialized: boolean = false;
29
+
30
+ constructor(config: DigitalRainConfig = {}) {
31
+ super();
32
+
33
+ this.#scale = config.scale ?? 1;
34
+ this.#speed = config.speed ?? 1;
35
+ this.#fontSize = (config.fontSize ?? 14) * this.#scale;
36
+ this.#maxColumns = config.columns ?? 0;
37
+ this.#mode = config.mode ?? 'hex';
38
+ this.#colorRGB = hexToRGB(config.color ?? '#00ffaa');
39
+ this.#trailLength = config.trailLength ?? 20;
40
+
41
+ if (innerWidth < 991 && this.#maxColumns > 0) {
42
+ this.#maxColumns = Math.floor(this.#maxColumns / 2);
43
+ }
44
+ }
45
+
46
+ onResize(width: number, height: number): void {
47
+ this.#width = width;
48
+ this.#height = height;
49
+
50
+ if (!this.#initialized) {
51
+ this.#initialized = true;
52
+ this.#columns = [];
53
+ this.#respawnTimers = [];
54
+
55
+ const columnWidth = this.#fontSize;
56
+ const totalSlots = Math.floor(width / columnWidth);
57
+ let columnCount: number;
58
+
59
+ if (this.#maxColumns === 0) {
60
+ columnCount = totalSlots;
61
+ } else {
62
+ columnCount = Math.min(this.#maxColumns, totalSlots);
63
+ }
64
+
65
+ if (innerWidth < 991 && this.#maxColumns === 0) {
66
+ columnCount = Math.floor(columnCount / 2);
67
+ }
68
+
69
+ for (let i = 0; i < columnCount; ++i) {
70
+ const column = this.#createColumn(totalSlots, height);
71
+ this.#columns.push(column);
72
+ this.#respawnTimers.push(0);
73
+ }
74
+ }
75
+ }
76
+
77
+ configure(config: Partial<DigitalRainConfig>): void {
78
+ if (config.speed !== undefined) {
79
+ this.#speed = config.speed;
80
+ }
81
+ if (config.trailLength !== undefined) {
82
+ this.#trailLength = config.trailLength;
83
+ }
84
+ }
85
+
86
+ tick(dt: number, width: number, height: number): void {
87
+ this.#width = width;
88
+ this.#height = height;
89
+
90
+ const columnWidth = this.#fontSize;
91
+ const totalSlots = Math.floor(width / columnWidth);
92
+
93
+ for (let i = 0; i < this.#columns.length; ++i) {
94
+ if (this.#respawnTimers[i] > 0) {
95
+ this.#respawnTimers[i] -= dt;
96
+
97
+ if (this.#respawnTimers[i] <= 0) {
98
+ this.#columns[i] = this.#createColumn(totalSlots, height);
99
+ }
100
+
101
+ continue;
102
+ }
103
+
104
+ const column = this.#columns[i];
105
+
106
+ column.y += column.speed * this.#speed * dt;
107
+ column.life += dt;
108
+
109
+ // Randomly change characters as they fall
110
+ for (let ci = 0; ci < column.chars.length; ++ci) {
111
+ if (MULBERRY.next() < 0.04) {
112
+ column.chars[ci] = this.#randomChar();
113
+ }
114
+ }
115
+
116
+ const topOfTrail = column.y - (column.chars.length - 1) * this.#fontSize;
117
+
118
+ if (topOfTrail > height) {
119
+ this.#respawnTimers[i] = 10 + MULBERRY.next() * 60;
120
+ }
121
+ }
122
+ }
123
+
124
+ draw(ctx: CanvasRenderingContext2D, width: number, height: number): void {
125
+ ctx.fillStyle = 'rgb(0, 0, 0)';
126
+ ctx.fillRect(0, 0, width, height);
127
+
128
+ const [cr, cg, cb] = this.#colorRGB;
129
+
130
+ ctx.font = `${this.#fontSize}px monospace`;
131
+ ctx.textAlign = 'center';
132
+
133
+ for (const column of this.#columns) {
134
+ const charCount = column.chars.length;
135
+
136
+ for (let ci = 0; ci < charCount; ++ci) {
137
+ const charY = column.y - (charCount - 1 - ci) * this.#fontSize;
138
+
139
+ if (charY < -this.#fontSize || charY > height + this.#fontSize) {
140
+ continue;
141
+ }
142
+
143
+ const isHead = ci === charCount - 1;
144
+
145
+ if (isHead) {
146
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.95)';
147
+ } else {
148
+ const trailProgress = ci / (charCount - 1);
149
+ const alpha = trailProgress * 0.8 + 0.05;
150
+ ctx.fillStyle = `rgba(${cr}, ${cg}, ${cb}, ${alpha})`;
151
+ }
152
+
153
+ ctx.fillText(column.chars[ci], column.x, charY);
154
+ }
155
+ }
156
+ }
157
+
158
+ #randomChar(): string {
159
+ let charset: string;
160
+
161
+ switch (this.#mode) {
162
+ case 'binary':
163
+ charset = BINARY_CHARS;
164
+ break;
165
+ case 'hex':
166
+ charset = HEX_CHARS;
167
+ break;
168
+ case 'mixed':
169
+ charset = MIXED_CHARS;
170
+ break;
171
+ }
172
+
173
+ return charset[Math.floor(MULBERRY.next() * charset.length)];
174
+ }
175
+
176
+ #createColumn(totalSlots: number, height: number): DigitalRainColumn {
177
+ const columnWidth = this.#fontSize;
178
+ const slot = Math.floor(MULBERRY.next() * totalSlots);
179
+ const length = Math.floor(this.#trailLength * 0.5 + MULBERRY.next() * this.#trailLength);
180
+ const chars: string[] = [];
181
+
182
+ for (let ci = 0; ci < length; ++ci) {
183
+ chars.push(this.#randomChar());
184
+ }
185
+
186
+ return {
187
+ x: slot * columnWidth + columnWidth / 2,
188
+ y: -(MULBERRY.next() * height),
189
+ speed: 1.5 + MULBERRY.next() * 3,
190
+ chars,
191
+ length,
192
+ life: 0
193
+ };
194
+ }
195
+ }
@@ -0,0 +1,10 @@
1
+ export type DigitalRainMode = 'binary' | 'hex' | 'mixed';
2
+
3
+ export type DigitalRainColumn = {
4
+ x: number;
5
+ y: number;
6
+ speed: number;
7
+ chars: string[];
8
+ length: number;
9
+ life: number;
10
+ };
@@ -135,7 +135,9 @@ export class Donuts extends Effect<DonutsConfig> {
135
135
  for (const donut of this.#donuts) {
136
136
  const cos = Math.cos(donut.angle);
137
137
  const sin = Math.sin(donut.angle);
138
- ctx.setTransform(cos, sin, -sin, cos, donut.x, donut.y);
138
+
139
+ ctx.save();
140
+ ctx.transform(cos, sin, -sin, cos, donut.x, donut.y);
139
141
 
140
142
  ctx.beginPath();
141
143
  ctx.arc(0, 0, donut.outerRadius, 0, Math.PI * 2);
@@ -144,9 +146,9 @@ export class Donuts extends Effect<DonutsConfig> {
144
146
 
145
147
  ctx.fillStyle = donut.color;
146
148
  ctx.fill();
147
- }
148
149
 
149
- ctx.resetTransform();
150
+ ctx.restore();
151
+ }
150
152
  }
151
153
 
152
154
  #updateDonut(donut: Donut, dt: number): void {
@@ -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 { Glitch } from './layer';
2
+ import type { GlitchConfig } from './types';
3
+ import type { Effect } from '../effect';
4
+
5
+ export function createGlitch(config?: GlitchConfig): Effect<GlitchConfig> {
6
+ return new Glitch(config);
7
+ }
8
+
9
+ export type { GlitchConfig, GlitchSlice, GlitchBlock } from './types';